diff --git a/common/src/main/java/bisq/common/app/Capability.java b/common/src/main/java/bisq/common/app/Capability.java index 8c5b2c62316..83e4d0f1136 100644 --- a/common/src/main/java/bisq/common/app/Capability.java +++ b/common/src/main/java/bisq/common/app/Capability.java @@ -31,5 +31,6 @@ public enum Capability { PROPOSAL, BLIND_VOTE, ACK_MSG, - BSQ_BLOCK + BSQ_BLOCK, + DAO_STATE } diff --git a/common/src/main/java/bisq/common/util/Utilities.java b/common/src/main/java/bisq/common/util/Utilities.java index 000a48c899a..11c72ac8a84 100644 --- a/common/src/main/java/bisq/common/util/Utilities.java +++ b/common/src/main/java/bisq/common/util/Utilities.java @@ -161,7 +161,7 @@ public static ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor(String public static boolean isMacMenuBarDarkMode() { try { // check for exit status only. Once there are more modes than "dark" and "default", we might need to analyze string contents.. - final Process process = Runtime.getRuntime().exec(new String[] {"defaults", "read", "-g", "AppleInterfaceStyle"}); + Process process = Runtime.getRuntime().exec(new String[]{"defaults", "read", "-g", "AppleInterfaceStyle"}); process.waitFor(100, TimeUnit.MILLISECONDS); return process.exitValue() == 0; } catch (IOException | InterruptedException | IllegalThreadStateException ex) { @@ -512,15 +512,29 @@ public static void checkCryptoPolicySetup() throws NoSuchAlgorithmException, Lim throw new LimitedKeyStrengthException(); } + public static String toTruncatedString(Object message) { + return toTruncatedString(message, 200, true); + } + public static String toTruncatedString(Object message, int maxLength) { - if (message != null) { - return StringUtils.abbreviate(message.toString(), maxLength).replace("\n", ""); - } - return "null"; + return toTruncatedString(message, maxLength, true); } - public static String toTruncatedString(Object message) { - return toTruncatedString(message, 200); + public static String toTruncatedString(Object message, boolean removeLinebreaks) { + return toTruncatedString(message, 200, removeLinebreaks); + } + + public static String toTruncatedString(Object message, int maxLength, boolean removeLinebreaks) { + if (message == null) + return "null"; + + + String result = StringUtils.abbreviate(message.toString(), maxLength); + if (removeLinebreaks) + return result.replace("\n", ""); + + return result; + } public static String getRandomPrefix(int minLength, int maxLength) { diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index 6f6adf5c0d8..e7d5a95ca8e 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -57,6 +57,15 @@ message NetworkEnvelope { AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 31; AckMessage ack_message = 32; RepublishGovernanceDataRequest republish_governance_data_request = 33; + NewDaoStateHashMessage new_dao_state_hash_message = 34; + GetDaoStateHashesRequest get_dao_state_hashes_request = 35; + GetDaoStateHashesResponse get_dao_state_hashes_response = 36; + NewProposalStateHashMessage new_proposal_state_hash_message = 37; + GetProposalStateHashesRequest get_proposal_state_hashes_request = 38; + GetProposalStateHashesResponse get_proposal_state_hashes_response = 39; + NewBlindVoteStateHashMessage new_blind_vote_state_hash_message = 40; + GetBlindVoteStateHashesRequest get_blind_vote_state_hashes_request = 41; + GetBlindVoteStateHashesResponse get_blind_vote_state_hashes_response = 42; } } @@ -324,6 +333,48 @@ message NewBlockBroadcastMessage { message RepublishGovernanceDataRequest { } +message NewDaoStateHashMessage { + DaoStateHash state_hash = 1; +} + +message NewProposalStateHashMessage { + ProposalStateHash state_hash = 1; +} + +message NewBlindVoteStateHashMessage { + BlindVoteStateHash state_hash = 1; +} + +message GetDaoStateHashesRequest { + int32 height = 1; + int32 nonce = 2; +} + +message GetProposalStateHashesRequest { + int32 height = 1; + int32 nonce = 2; +} + +message GetBlindVoteStateHashesRequest { + int32 height = 1; + int32 nonce = 2; +} + +message GetDaoStateHashesResponse { + repeated DaoStateHash state_hashes = 1; + int32 request_nonce = 2; +} + +message GetProposalStateHashesResponse { + repeated ProposalStateHash state_hashes = 1; + int32 request_nonce = 2; +} + +message GetBlindVoteStateHashesResponse { + repeated BlindVoteStateHash state_hashes = 1; + int32 request_nonce = 2; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Payload /////////////////////////////////////////////////////////////////////////////////////////// @@ -915,7 +966,6 @@ message AdvancedCashAccountPayload { string account_nr = 1; } - /////////////////////////////////////////////////////////////////////////////////////////// // PersistableEnvelope /////////////////////////////////////////////////////////////////////////////////////////// @@ -1713,6 +1763,27 @@ message DecryptedBallotsWithMerits { message DaoStateStore { BsqState bsq_state = 1; + repeated DaoStateHash dao_state_hash = 2; +} + +message DaoStateHash { + int32 height = 1; + bytes hash = 2; + bytes prev_hash = 3; +} + +message ProposalStateHash { + int32 height = 1; + bytes hash = 2; + bytes prev_hash = 3; + int32 num_proposals = 4; +} + +message BlindVoteStateHash { + int32 height = 1; + bytes hash = 2; + bytes prev_hash = 3; + int32 num_blind_votes = 4; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/btc/BitcoinModule.java b/core/src/main/java/bisq/core/btc/BitcoinModule.java index bb4af9fafb1..60c55d31c65 100644 --- a/core/src/main/java/bisq/core/btc/BitcoinModule.java +++ b/core/src/main/java/bisq/core/btc/BitcoinModule.java @@ -56,9 +56,12 @@ public BitcoinModule(Environment environment) { protected void configure() { // We we have selected BTC_DAO_TESTNET we use our master regtest node, otherwise the specified host or default // (localhost) - String regTestHost = BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet() ? - "104.248.31.39" : - environment.getProperty(BtcOptionKeys.REG_TEST_HOST, String.class, RegTestHost.DEFAULT_HOST); + String regTestHost = environment.getProperty(BtcOptionKeys.REG_TEST_HOST, String.class, ""); + if (regTestHost.isEmpty()) { + regTestHost = BisqEnvironment.getBaseCurrencyNetwork().isDaoTestNet() ? + "104.248.31.39" : + RegTestHost.DEFAULT_HOST; + } RegTestHost.HOST = regTestHost; if (Arrays.asList("localhost", "127.0.0.1").contains(regTestHost)) { diff --git a/core/src/main/java/bisq/core/dao/DaoEventCoordinator.java b/core/src/main/java/bisq/core/dao/DaoEventCoordinator.java new file mode 100644 index 00000000000..4c8cc0f4098 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/DaoEventCoordinator.java @@ -0,0 +1,69 @@ +/* + * 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; + +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.dao.state.model.blockchain.Block; + +import javax.inject.Inject; + +public class DaoEventCoordinator implements DaoSetupService, DaoStateListener { + private final DaoStateService daoStateService; + private final DaoStateSnapshotService daoStateSnapshotService; + private final DaoStateMonitoringService daoStateMonitoringService; + + @Inject + public DaoEventCoordinator(DaoStateService daoStateService, + DaoStateSnapshotService daoStateSnapshotService, + DaoStateMonitoringService daoStateMonitoringService) { + this.daoStateService = daoStateService; + this.daoStateSnapshotService = daoStateSnapshotService; + this.daoStateMonitoringService = daoStateMonitoringService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + this.daoStateService.addDaoStateListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + // We listen onDaoStateChanged to ensure the dao state has been processed from listener clients after parsing. + // We need to listen during batch processing as well to write snapshots during that process. + @Override + public void onDaoStateChanged(Block block) { + // We need to execute first the daoStateMonitoringService + daoStateMonitoringService.createHashFromBlock(block); + daoStateSnapshotService.maybeCreateSnapshot(block); + } +} diff --git a/core/src/main/java/bisq/core/dao/DaoModule.java b/core/src/main/java/bisq/core/dao/DaoModule.java index 4d394bb2e43..1641b754968 100644 --- a/core/src/main/java/bisq/core/dao/DaoModule.java +++ b/core/src/main/java/bisq/core/dao/DaoModule.java @@ -64,6 +64,12 @@ import bisq.core.dao.governance.voteresult.VoteResultService; import bisq.core.dao.governance.voteresult.issuance.IssuanceService; import bisq.core.dao.governance.votereveal.VoteRevealService; +import bisq.core.dao.monitoring.BlindVoteStateMonitoringService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.ProposalStateMonitoringService; +import bisq.core.dao.monitoring.network.BlindVoteStateNetworkService; +import bisq.core.dao.monitoring.network.DaoStateNetworkService; +import bisq.core.dao.monitoring.network.ProposalStateNetworkService; import bisq.core.dao.node.BsqNodeProvider; import bisq.core.dao.node.explorer.ExportJsonFilesService; import bisq.core.dao.node.full.FullNode; @@ -99,6 +105,7 @@ public DaoModule(Environment environment) { protected void configure() { bind(DaoSetup.class).in(Singleton.class); bind(DaoFacade.class).in(Singleton.class); + bind(DaoEventCoordinator.class).in(Singleton.class); bind(DaoKillSwitch.class).in(Singleton.class); // Node, parser @@ -116,6 +123,12 @@ protected void configure() { bind(DaoStateService.class).in(Singleton.class); bind(DaoStateSnapshotService.class).in(Singleton.class); bind(DaoStateStorageService.class).in(Singleton.class); + bind(DaoStateMonitoringService.class).in(Singleton.class); + bind(DaoStateNetworkService.class).in(Singleton.class); + bind(ProposalStateMonitoringService.class).in(Singleton.class); + bind(ProposalStateNetworkService.class).in(Singleton.class); + bind(BlindVoteStateMonitoringService.class).in(Singleton.class); + bind(BlindVoteStateNetworkService.class).in(Singleton.class); bind(UnconfirmedBsqChangeOutputListService.class).in(Singleton.class); bind(ExportJsonFilesService.class).in(Singleton.class); diff --git a/core/src/main/java/bisq/core/dao/DaoSetup.java b/core/src/main/java/bisq/core/dao/DaoSetup.java index 591f65af26f..550b64d867f 100644 --- a/core/src/main/java/bisq/core/dao/DaoSetup.java +++ b/core/src/main/java/bisq/core/dao/DaoSetup.java @@ -31,6 +31,9 @@ import bisq.core.dao.governance.voteresult.MissingDataRequestService; import bisq.core.dao.governance.voteresult.VoteResultService; import bisq.core.dao.governance.votereveal.VoteRevealService; +import bisq.core.dao.monitoring.BlindVoteStateMonitoringService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.ProposalStateMonitoringService; import bisq.core.dao.node.BsqNode; import bisq.core.dao.node.BsqNodeProvider; import bisq.core.dao.node.explorer.ExportJsonFilesService; @@ -69,11 +72,20 @@ public DaoSetup(BsqNodeProvider bsqNodeProvider, ProofOfBurnService proofOfBurnService, DaoFacade daoFacade, ExportJsonFilesService exportJsonFilesService, - DaoKillSwitch daoKillSwitch) { + DaoKillSwitch daoKillSwitch, + DaoStateMonitoringService daoStateMonitoringService, + ProposalStateMonitoringService proposalStateMonitoringService, + BlindVoteStateMonitoringService blindVoteStateMonitoringService, + DaoEventCoordinator daoEventCoordinator) { bsqNode = bsqNodeProvider.getBsqNode(); // We need to take care of order of execution. + + // For order critical event flow we use the daoEventCoordinator to delegate the calls from anonymous listeners + // to concrete clients. + daoSetupServices.add(daoEventCoordinator); + daoSetupServices.add(daoStateService); daoSetupServices.add(cycleService); daoSetupServices.add(ballotListService); @@ -92,6 +104,10 @@ public DaoSetup(BsqNodeProvider bsqNodeProvider, daoSetupServices.add(daoFacade); daoSetupServices.add(exportJsonFilesService); daoSetupServices.add(daoKillSwitch); + daoSetupServices.add(daoStateMonitoringService); + daoSetupServices.add(proposalStateMonitoringService); + daoSetupServices.add(blindVoteStateMonitoringService); + daoSetupServices.add(bsqNodeProvider.getBsqNode()); } 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 6e1219cf01e..eb920dcbc61 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 @@ -105,7 +105,7 @@ public void onParseBlockChainComplete() { @Override public void onAdded(PersistableNetworkPayload payload) { - onAppendOnlyDataAdded(payload); + onAppendOnlyDataAdded(payload, true); } @@ -120,16 +120,23 @@ public List getBlindVotesInPhaseAndCycle() { .collect(Collectors.toList()); } + public List getConfirmedBlindVotes() { + return blindVotePayloads.stream() + .filter(blindVotePayload -> blindVoteValidator.areDataFieldsValidAndTxConfirmed(blindVotePayload.getBlindVote())) + .map(BlindVotePayload::getBlindVote) + .collect(Collectors.toList()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void fillListFromAppendOnlyDataStore() { - p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(this::onAppendOnlyDataAdded); + p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(e -> onAppendOnlyDataAdded(e, false)); } - private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload) { + private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean doLog) { if (persistableNetworkPayload instanceof BlindVotePayload) { BlindVotePayload blindVotePayload = (BlindVotePayload) persistableNetworkPayload; if (!blindVotePayloads.contains(blindVotePayload)) { @@ -140,7 +147,9 @@ private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkP if (blindVoteValidator.areDataFieldsValid(blindVote)) { // We don't validate as we might receive blindVotes from other cycles or phases at startup. blindVotePayloads.add(blindVotePayload); - log.info("We received a blindVotePayload. blindVoteTxId={}", txId); + if (doLog) { + log.info("We received a blindVotePayload. blindVoteTxId={}", txId); + } } else { log.warn("We received an invalid blindVotePayload. blindVoteTxId={}", txId); } diff --git a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteList.java b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteList.java index cbd544e550e..a66f610ced3 100644 --- a/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteList.java +++ b/core/src/main/java/bisq/core/dao/governance/blindvote/MyBlindVoteList.java @@ -47,7 +47,7 @@ public class MyBlindVoteList extends PersistableList implements Conse // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private MyBlindVoteList(List list) { + public MyBlindVoteList(List list) { super(list); } 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 fe11def2a70..d7af25ebbe7 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 @@ -145,6 +145,7 @@ public MyBlindVoteListService(P2PService p2PService, @Override public void addListeners() { daoStateService.addDaoStateListener(this); + p2PService.getNumConnectedPeers().addListener(numConnectedPeersListener); } @Override diff --git a/core/src/main/java/bisq/core/dao/governance/period/CycleService.java b/core/src/main/java/bisq/core/dao/governance/period/CycleService.java index e869c006c39..65d8540caae 100644 --- a/core/src/main/java/bisq/core/dao/governance/period/CycleService.java +++ b/core/src/main/java/bisq/core/dao/governance/period/CycleService.java @@ -79,7 +79,7 @@ public void start() { public void onNewBlockHeight(int blockHeight) { if (blockHeight != genesisBlockHeight) maybeCreateNewCycle(blockHeight, daoStateService.getCycles()) - .ifPresent(daoStateService.getCycles()::add); + .ifPresent(daoStateService::addCycle); } @@ -88,7 +88,7 @@ public void onNewBlockHeight(int blockHeight) { /////////////////////////////////////////////////////////////////////////////////////////// public void addFirstCycle() { - daoStateService.getCycles().add(getFirstCycle()); + daoStateService.addCycle(getFirstCycle()); } public int getCycleIndex(Cycle cycle) { diff --git a/core/src/main/java/bisq/core/dao/governance/period/PeriodService.java b/core/src/main/java/bisq/core/dao/governance/period/PeriodService.java index f175183b27f..18ea9b61912 100644 --- a/core/src/main/java/bisq/core/dao/governance/period/PeriodService.java +++ b/core/src/main/java/bisq/core/dao/governance/period/PeriodService.java @@ -85,7 +85,7 @@ public boolean isLastBlockInCycle(int height) { .isPresent(); } - private Optional getCycle(int height) { + public Optional getCycle(int height) { return daoStateService.getCycle(height); } diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalList.java b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalList.java index 5f1931a269d..948d18f14d0 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalList.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/MyProposalList.java @@ -31,12 +31,12 @@ import lombok.EqualsAndHashCode; /** - * PersistableEnvelope wrapper for list of ballots. Used in vote consensus, so changes can break consensus! + * PersistableEnvelope wrapper for list of proposals. Used in vote consensus, so changes can break consensus! */ @EqualsAndHashCode(callSuper = true) public class MyProposalList extends PersistableList implements ConsensusCritical { - private MyProposalList(List list) { + public MyProposalList(List list) { super(list); } 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 486235555ea..b7f36d52a5a 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 @@ -96,6 +96,7 @@ public MyProposalListService(P2PService p2PService, numConnectedPeersListener = (observable, oldValue, newValue) -> rePublishOnceWellConnected(); daoStateService.addDaoStateListener(this); + p2PService.getNumConnectedPeers().addListener(numConnectedPeersListener); } @@ -130,9 +131,6 @@ public void onParseBlockChainComplete() { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void start() { - } - // Broadcast tx and publish proposal to P2P network public void publishTxAndPayload(Proposal proposal, Transaction transaction, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { 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 c38ea0dd476..741d8a958be 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 @@ -131,7 +131,7 @@ public void start() { @Override public void onAdded(ProtectedStorageEntry entry) { - onProtectedDataAdded(entry); + onProtectedDataAdded(entry, true); } @Override @@ -146,7 +146,7 @@ public void onRemoved(ProtectedStorageEntry entry) { @Override public void onAdded(PersistableNetworkPayload payload) { - onAppendOnlyDataAdded(payload); + onAppendOnlyDataAdded(payload, true); } @@ -190,11 +190,11 @@ public List getValidatedProposals() { /////////////////////////////////////////////////////////////////////////////////////////// private void fillListFromProtectedStore() { - p2PService.getDataMap().values().forEach(this::onProtectedDataAdded); + p2PService.getDataMap().values().forEach(e -> onProtectedDataAdded(e, false)); } private void fillListFromAppendOnlyDataStore() { - p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(this::onAppendOnlyDataAdded); + p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(e -> onAppendOnlyDataAdded(e, false)); } private void publishToAppendOnlyDataStore() { @@ -211,7 +211,7 @@ private void publishToAppendOnlyDataStore() { }); } - private void onProtectedDataAdded(ProtectedStorageEntry entry) { + private void onProtectedDataAdded(ProtectedStorageEntry entry, boolean doLog) { ProtectedStoragePayload protectedStoragePayload = entry.getProtectedStoragePayload(); if (protectedStoragePayload instanceof TempProposalPayload) { Proposal proposal = ((TempProposalPayload) protectedStoragePayload).getProposal(); @@ -219,8 +219,10 @@ private void onProtectedDataAdded(ProtectedStorageEntry entry) { // available/confirmed. But we check if we are in the proposal phase. if (!tempProposals.contains(proposal)) { if (proposalValidator.isValidOrUnconfirmed(proposal)) { - log.info("We received a TempProposalPayload and store it to our protectedStoreList. proposalTxId={}", - proposal.getTxId()); + if (doLog) { + 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={}", @@ -256,14 +258,16 @@ private void onProtectedDataRemoved(ProtectedStorageEntry entry) { } } - private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload) { + private void onAppendOnlyDataAdded(PersistableNetworkPayload persistableNetworkPayload, boolean doLog) { if (persistableNetworkPayload instanceof ProposalPayload) { ProposalPayload proposalPayload = (ProposalPayload) persistableNetworkPayload; if (!proposalPayloads.contains(proposalPayload)) { Proposal proposal = proposalPayload.getProposal(); if (proposalValidator.areDataFieldsValid(proposal)) { - log.info("We received a ProposalPayload and store it to our appendOnlyStoreList. proposalTxId={}", - proposal.getTxId()); + if (doLog) { + log.info("We received a ProposalPayload and store it to our appendOnlyStoreList. proposalTxId={}", + proposal.getTxId()); + } proposalPayloads.add(proposalPayload); } else { log.warn("We received a invalid append-only proposal from the P2P network. " + 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 11524b39632..b82c666e23f 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 @@ -235,6 +235,7 @@ private void maybeCalculateVoteResult(int chainHeight) { } // Those which did not get accepted will be added to the nonBsq map + // FIXME add check for cycle as now we call addNonBsqTxOutput for past rejected comp requests as well daoStateService.getIssuanceCandidateTxOutputs().stream() .filter(txOutput -> !daoStateService.isIssuanceTx(txOutput.getTxId())) .forEach(daoStateService::addNonBsqTxOutput); @@ -304,6 +305,8 @@ private Function txOutputToDecryptedBallot return getDecryptedBallotsWithMerits(voteRevealTxId, currentCycle, voteRevealOpReturnData, blindVoteTxId, hashOfBlindVoteList, blindVoteStake, optionalBlindVote.get()); } + + // We are missing P2P network data return getEmptyDecryptedBallotsWithMerits(voteRevealTxId, blindVoteTxId, hashOfBlindVoteList, blindVoteStake); } catch (Throwable e) { diff --git a/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java new file mode 100644 index 00000000000..738a82f3aa6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/BlindVoteStateMonitoringService.java @@ -0,0 +1,317 @@ +/* + * 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; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.blindvote.BlindVote; +import bisq.core.dao.governance.blindvote.BlindVoteListService; +import bisq.core.dao.governance.blindvote.MyBlindVoteList; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.monitoring.model.BlindVoteStateBlock; +import bisq.core.dao.monitoring.model.BlindVoteStateHash; +import bisq.core.dao.monitoring.network.BlindVoteStateNetworkService; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage; +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.Block; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DaoPhase; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; + +import bisq.common.UserThread; +import bisq.common.crypto.Hash; + +import javax.inject.Inject; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Monitors the BlindVote P2P network payloads with using a hash of a sorted list of BlindVotes from one cycle and + * make it accessible to the network so we can detect quickly if any consensus issue arise. + * We create that hash at the first block of the VoteReveal phase. There is one hash created per cycle. + * The hash contains the hash of the previous block so we can ensure the validity of the whole history by + * comparing the last block. + * + * We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start + * to listen for broadcast messages from our peers about dao state of new blocks. + * + * We do NOT persist that chain of hashes as there is only one per cycle and the performance costs are very low. + */ +@Slf4j +public class BlindVoteStateMonitoringService implements DaoSetupService, DaoStateListener, BlindVoteStateNetworkService.Listener { + public interface Listener { + void onBlindVoteStateBlockChainChanged(); + } + + private final DaoStateService daoStateService; + private final BlindVoteStateNetworkService blindVoteStateNetworkService; + private final GenesisTxInfo genesisTxInfo; + private final PeriodService periodService; + private final BlindVoteListService blindVoteListService; + + @Getter + private final LinkedList blindVoteStateBlockChain = new LinkedList<>(); + @Getter + private final LinkedList blindVoteStateHashChain = new LinkedList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + @Getter + private boolean isInConflict; + private boolean parseBlockChainComplete; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BlindVoteStateMonitoringService(DaoStateService daoStateService, + BlindVoteStateNetworkService blindVoteStateNetworkService, + GenesisTxInfo genesisTxInfo, + PeriodService periodService, + BlindVoteListService blindVoteListService) { + this.daoStateService = daoStateService; + this.blindVoteStateNetworkService = blindVoteStateNetworkService; + this.genesisTxInfo = genesisTxInfo; + this.periodService = periodService; + this.blindVoteListService = blindVoteListService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + blindVoteStateNetworkService.addListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("Duplicates") + @Override + public void onDaoStateChanged(Block block) { + int blockHeight = block.getHeight(); + + int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight(); + + if (blindVoteStateBlockChain.isEmpty() && blockHeight > genesisBlockHeight) { + // Takes about 150 ms for dao testnet data + long ts = System.currentTimeMillis(); + for (int i = genesisBlockHeight; i < blockHeight; i++) { + maybeUpdateHashChain(i); + } + log.info("updateHashChain for {} items took {} ms", + blockHeight - genesisBlockHeight, + System.currentTimeMillis() - ts); + } + maybeUpdateHashChain(blockHeight); + } + + @SuppressWarnings("Duplicates") + @Override + public void onParseBlockChainComplete() { + parseBlockChainComplete = true; + blindVoteStateNetworkService.addListeners(); + + // 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 + // period is arbitrary anyway... + Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); + int fromHeight = Math.max(genesisTxInfo.getGenesisBlockHeight(), daoStateService.getChainHeight() - currentCycle.getDuration() * 5); + + blindVoteStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // StateNetworkService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewStateHashMessage(NewBlindVoteStateHashMessage newStateHashMessage, Connection connection) { + if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) { + processPeersBlindVoteStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true); + } + } + + @Override + public void onGetStateHashRequest(Connection connection, GetBlindVoteStateHashesRequest getStateHashRequest) { + int fromHeight = getStateHashRequest.getHeight(); + List blindVoteStateHashes = blindVoteStateBlockChain.stream() + .filter(e -> e.getHeight() >= fromHeight) + .map(BlindVoteStateBlock::getMyStateHash) + .collect(Collectors.toList()); + blindVoteStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), blindVoteStateHashes); + } + + @Override + public void onPeersStateHashes(List stateHashes, Optional peersNodeAddress) { + AtomicBoolean hasChanged = new AtomicBoolean(false); + stateHashes.forEach(daoStateHash -> { + boolean changed = processPeersBlindVoteStateHash(daoStateHash, peersNodeAddress, false); + if (changed) { + hasChanged.set(true); + } + }); + + if (hasChanged.get()) { + listeners.forEach(Listener::onBlindVoteStateBlockChainChanged); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void requestHashesFromGenesisBlockHeight(String peersAddress) { + blindVoteStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void maybeUpdateHashChain(int blockHeight) { + // We use first block in blind vote phase to create the hash of our blindVotes. We prefer to wait as long as + // possible to increase the chance that we have received all blindVotes. + if (!isFirstBlockOfBlindVotePhase(blockHeight)) { + return; + } + + periodService.getCycle(blockHeight).ifPresent(cycle -> { + List blindVotes = blindVoteListService.getConfirmedBlindVotes().stream() + .filter(e -> periodService.isTxInCorrectCycle(e.getTxId(), blockHeight)) + .sorted(Comparator.comparing(BlindVote::getTxId)).collect(Collectors.toList()); + + // We use MyBlindVoteList to get the serialized bytes from the blindVotes list + byte[] serializedBlindVotes = new MyBlindVoteList(blindVotes).toProtoMessage().toByteArray(); + + byte[] prevHash; + if (blindVoteStateBlockChain.isEmpty()) { + prevHash = new byte[0]; + } else { + prevHash = blindVoteStateBlockChain.getLast().getHash(); + } + byte[] combined = ArrayUtils.addAll(prevHash, serializedBlindVotes); + byte[] hash = Hash.getSha256Ripemd160hash(combined); + + + BlindVoteStateHash myBlindVoteStateHash = new BlindVoteStateHash(blockHeight, hash, prevHash, blindVotes.size()); + BlindVoteStateBlock blindVoteStateBlock = new BlindVoteStateBlock(myBlindVoteStateHash); + blindVoteStateBlockChain.add(blindVoteStateBlock); + blindVoteStateHashChain.add(myBlindVoteStateHash); + + // We only broadcast after parsing of blockchain is complete + if (parseBlockChainComplete) { + // We notify listeners only after batch processing to avoid performance issues at UI code + listeners.forEach(Listener::onBlindVoteStateBlockChainChanged); + + // We delay broadcast to give peers enough time to have received the block. + // Otherwise they would ignore our data if received block is in future to their local blockchain. + int delayInSec = 5 + new Random().nextInt(10); + UserThread.runAfter(() -> blindVoteStateNetworkService.broadcastMyStateHash(myBlindVoteStateHash), delayInSec); + } + }); + } + + private boolean processPeersBlindVoteStateHash(BlindVoteStateHash blindVoteStateHash, Optional peersNodeAddress, boolean notifyListeners) { + AtomicBoolean changed = new AtomicBoolean(false); + AtomicBoolean isInConflict = new AtomicBoolean(this.isInConflict); + StringBuilder sb = new StringBuilder(); + blindVoteStateBlockChain.stream() + .filter(e -> e.getHeight() == blindVoteStateHash.getHeight()).findAny() + .ifPresent(daoStateBlock -> { + String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress) + .orElseGet(() -> "Unknown peer " + new Random().nextInt(10000)); + daoStateBlock.putInPeersMap(peersNodeAddressAsString, blindVoteStateHash); + if (!daoStateBlock.getMyStateHash().hasEqualHash(blindVoteStateHash)) { + daoStateBlock.putInConflictMap(peersNodeAddressAsString, blindVoteStateHash); + isInConflict.set(true); + sb.append("We received a block hash from peer ") + .append(peersNodeAddressAsString) + .append(" which conflicts with our block hash.\n") + .append("my blindVoteStateHash=") + .append(daoStateBlock.getMyStateHash()) + .append("\npeers blindVoteStateHash=") + .append(blindVoteStateHash); + } + changed.set(true); + }); + + this.isInConflict = isInConflict.get(); + + String conflictMsg = sb.toString(); + if (this.isInConflict && !conflictMsg.isEmpty()) { + log.warn(conflictMsg); + } + + if (notifyListeners && changed.get()) { + listeners.forEach(Listener::onBlindVoteStateBlockChainChanged); + } + + return changed.get(); + } + + private boolean isFirstBlockOfBlindVotePhase(int blockHeight) { + return blockHeight == periodService.getFirstBlockOfPhase(blockHeight, DaoPhase.Phase.VOTE_REVEAL); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java new file mode 100644 index 00000000000..f3fb79544da --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java @@ -0,0 +1,298 @@ +/* + * 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; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.monitoring.model.DaoStateBlock; +import bisq.core.dao.monitoring.model.DaoStateHash; +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.Block; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; + +import bisq.common.UserThread; +import bisq.common.crypto.Hash; + +import javax.inject.Inject; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Monitors the DaoState with using a hash fo the complete daoState and make it accessible to the network + * so we can detect quickly if any consensus issue arise. + * We create that hash after parsing and processing of a block is completed. There is one hash created per block. + * The hash contains the hash of the previous block so we can ensure the validity of the whole history by + * comparing the last block. + * + * We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start + * to listen for broadcast messages from our peers about dao state of new blocks. It could be that the received dao + * state from the peers is already covering the next block we have not received yet. So we only take data in account + * which are inside the block height we have already. To avoid such race conditions we delay the broadcasting of our + * state to the peers to not get ignored it in case they have not received the block yet. + * + * We do persist that chain of hashes with the snapshot. + */ +@Slf4j +public class DaoStateMonitoringService implements DaoSetupService, DaoStateListener, + DaoStateNetworkService.Listener { + + public interface Listener { + void onChangeAfterBatchProcessing(); + } + + private final DaoStateService daoStateService; + private final DaoStateNetworkService daoStateNetworkService; + private final GenesisTxInfo genesisTxInfo; + + @Getter + private final LinkedList daoStateBlockChain = new LinkedList<>(); + @Getter + private final LinkedList daoStateHashChain = new LinkedList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + private boolean parseBlockChainComplete; + @Getter + private boolean isInConflict; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DaoStateMonitoringService(DaoStateService daoStateService, + DaoStateNetworkService daoStateNetworkService, + GenesisTxInfo genesisTxInfo) { + this.daoStateService = daoStateService; + this.daoStateNetworkService = daoStateNetworkService; + this.genesisTxInfo = genesisTxInfo; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + daoStateNetworkService.addListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + // We do not use onDaoStateChanged but let the DaoEventCoordinator call createHashFromBlock to ensure the + // correct order of execution. + + @Override + public void onParseBlockChainComplete() { + parseBlockChainComplete = true; + daoStateNetworkService.addListeners(); + + // We wait for processing messages until we have completed batch processing + int fromHeight = daoStateService.getChainHeight() - 10; + daoStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // StateNetworkService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewStateHashMessage(NewDaoStateHashMessage newStateHashMessage, Connection connection) { + if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) { + processPeersDaoStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true); + } + } + + @Override + public void onGetStateHashRequest(Connection connection, GetDaoStateHashesRequest getStateHashRequest) { + int fromHeight = getStateHashRequest.getHeight(); + List daoStateHashes = daoStateBlockChain.stream() + .filter(e -> e.getHeight() >= fromHeight) + .map(DaoStateBlock::getMyStateHash) + .collect(Collectors.toList()); + daoStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), daoStateHashes); + } + + @Override + public void onPeersStateHashes(List stateHashes, Optional peersNodeAddress) { + AtomicBoolean hasChanged = new AtomicBoolean(false); + + stateHashes.forEach(daoStateHash -> { + boolean changed = processPeersDaoStateHash(daoStateHash, peersNodeAddress, false); + if (changed) { + hasChanged.set(true); + } + }); + + if (hasChanged.get()) { + listeners.forEach(Listener::onChangeAfterBatchProcessing); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void createHashFromBlock(Block block) { + updateHashChain(block); + } + + public void requestHashesFromGenesisBlockHeight(String peersAddress) { + daoStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress); + } + + public void applySnapshot(LinkedList persistedDaoStateHashChain) { + // We could got a reset from a reorg, so we clear all and start over from the genesis block. + daoStateHashChain.clear(); + daoStateBlockChain.clear(); + daoStateNetworkService.reset(); + + if (!persistedDaoStateHashChain.isEmpty()) { + log.info("Apply snapshot with {} daoStateHashes. Last daoStateHash={}", + persistedDaoStateHashChain.size(), persistedDaoStateHashChain.getLast()); + } + daoStateHashChain.addAll(persistedDaoStateHashChain); + daoStateHashChain.forEach(e -> daoStateBlockChain.add(new DaoStateBlock(e))); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateHashChain(Block block) { + byte[] prevHash; + int height = block.getHeight(); + if (daoStateBlockChain.isEmpty()) { + // Only at genesis we allow an empty prevHash + if (height == genesisTxInfo.getGenesisBlockHeight()) { + prevHash = new byte[0]; + } else { + log.warn("DaoStateBlockchain is empty but we received the block which was not the genesis block. " + + "We stop execution here."); + return; + } + } else { + checkArgument(height == daoStateBlockChain.getLast().getHeight() + 1, + "New block must be 1 block above previous block. height={}, " + + "daoStateBlockChain.getLast().getHeight()={}", + height, daoStateBlockChain.getLast().getHeight()); + prevHash = daoStateBlockChain.getLast().getHash(); + } + byte[] stateHash = daoStateService.getSerializedDaoState(); + // We include the prev. hash in our new hash so we can be sure that if one hash is matching all the past would + // match as well. + byte[] combined = ArrayUtils.addAll(prevHash, stateHash); + byte[] hash = Hash.getSha256Ripemd160hash(combined); + + DaoStateHash myDaoStateHash = new DaoStateHash(height, hash, prevHash); + DaoStateBlock daoStateBlock = new DaoStateBlock(myDaoStateHash); + daoStateBlockChain.add(daoStateBlock); + daoStateHashChain.add(myDaoStateHash); + + // We only broadcast after parsing of blockchain is complete + if (parseBlockChainComplete) { + // We notify listeners only after batch processing to avoid performance issues at UI code + listeners.forEach(Listener::onChangeAfterBatchProcessing); + + // We delay broadcast to give peers enough time to have received the block. + // Otherwise they would ignore our data if received block is in future to their local blockchain. + int delayInSec = 5 + new Random().nextInt(10); + UserThread.runAfter(() -> daoStateNetworkService.broadcastMyStateHash(myDaoStateHash), delayInSec); + } + } + + private boolean processPeersDaoStateHash(DaoStateHash daoStateHash, Optional peersNodeAddress, boolean notifyListeners) { + AtomicBoolean changed = new AtomicBoolean(false); + AtomicBoolean isInConflict = new AtomicBoolean(this.isInConflict); + StringBuilder sb = new StringBuilder(); + daoStateBlockChain.stream() + .filter(e -> e.getHeight() == daoStateHash.getHeight()).findAny() + .ifPresent(daoStateBlock -> { + String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress) + .orElseGet(() -> "Unknown peer " + new Random().nextInt(10000)); + daoStateBlock.putInPeersMap(peersNodeAddressAsString, daoStateHash); + if (!daoStateBlock.getMyStateHash().hasEqualHash(daoStateHash)) { + daoStateBlock.putInConflictMap(peersNodeAddressAsString, daoStateHash); + isInConflict.set(true); + sb.append("We received a block hash from peer ") + .append(peersNodeAddressAsString) + .append(" which conflicts with our block hash.\n") + .append("my daoStateHash=") + .append(daoStateBlock.getMyStateHash()) + .append("\npeers daoStateHash=") + .append(daoStateHash); + } + changed.set(true); + }); + + this.isInConflict = isInConflict.get(); + + String conflictMsg = sb.toString(); + if (this.isInConflict && !conflictMsg.isEmpty()) { + log.warn(conflictMsg); + } + + if (notifyListeners && changed.get()) { + listeners.forEach(Listener::onChangeAfterBatchProcessing); + } + + return changed.get(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java new file mode 100644 index 00000000000..84a68f70ed0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/ProposalStateMonitoringService.java @@ -0,0 +1,313 @@ +/* + * 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; + +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.MyProposalList; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.monitoring.model.ProposalStateBlock; +import bisq.core.dao.monitoring.model.ProposalStateHash; +import bisq.core.dao.monitoring.network.ProposalStateNetworkService; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage; +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.Block; +import bisq.core.dao.state.model.governance.Cycle; +import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.Proposal; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; + +import bisq.common.UserThread; +import bisq.common.crypto.Hash; + +import javax.inject.Inject; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Monitors the Proposal P2P network payloads with using a hash of a sorted list of Proposals from one cycle and + * make it accessible to the network so we can detect quickly if any consensus issue arise. + * We create that hash at the first block of the BlindVote phase. There is one hash created per cycle. + * The hash contains the hash of the previous block so we can ensure the validity of the whole history by + * comparing the last block. + * + * We request the state from the connected seed nodes after batch processing of BSQ is complete as well as we start + * to listen for broadcast messages from our peers about dao state of new blocks. + * + * We do NOT persist that chain of hashes as there is only one per cycle and the performance costs are very low. + */ +@Slf4j +public class ProposalStateMonitoringService implements DaoSetupService, DaoStateListener, ProposalStateNetworkService.Listener { + public interface Listener { + void onProposalStateBlockChainChanged(); + } + + private final DaoStateService daoStateService; + private final ProposalStateNetworkService proposalStateNetworkService; + private final GenesisTxInfo genesisTxInfo; + private final PeriodService periodService; + private final ProposalService proposalService; + + @Getter + private final LinkedList proposalStateBlockChain = new LinkedList<>(); + @Getter + private final LinkedList proposalStateHashChain = new LinkedList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); + @Getter + private boolean isInConflict; + private boolean parseBlockChainComplete; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProposalStateMonitoringService(DaoStateService daoStateService, + ProposalStateNetworkService proposalStateNetworkService, + GenesisTxInfo genesisTxInfo, + PeriodService periodService, + ProposalService proposalService) { + this.daoStateService = daoStateService; + this.proposalStateNetworkService = proposalStateNetworkService; + this.genesisTxInfo = genesisTxInfo; + this.periodService = periodService; + this.proposalService = proposalService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addDaoStateListener(this); + proposalStateNetworkService.addListener(this); + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("Duplicates") + public void onDaoStateChanged(Block block) { + int blockHeight = block.getHeight(); + int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight(); + + if (proposalStateBlockChain.isEmpty() && blockHeight > genesisBlockHeight) { + // Takes about 150 ms for dao testnet data + long ts = System.currentTimeMillis(); + for (int i = genesisBlockHeight; i < blockHeight; i++) { + maybeUpdateHashChain(i); + } + log.info("updateHashChain for {} items took {} ms", + blockHeight - genesisBlockHeight, + System.currentTimeMillis() - ts); + } + maybeUpdateHashChain(blockHeight); + } + + @SuppressWarnings("Duplicates") + @Override + public void onParseBlockChainComplete() { + parseBlockChainComplete = true; + proposalStateNetworkService.addListeners(); + + // 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 + // period is arbitrary anyway... + Cycle currentCycle = periodService.getCurrentCycle(); + checkNotNull(currentCycle, "currentCycle must not be null"); + int fromHeight = Math.max(genesisTxInfo.getGenesisBlockHeight(), daoStateService.getChainHeight() - currentCycle.getDuration() * 5); + + proposalStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // StateNetworkService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewStateHashMessage(NewProposalStateHashMessage newStateHashMessage, Connection connection) { + if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) { + processPeersProposalStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true); + } + } + + @Override + public void onGetStateHashRequest(Connection connection, GetProposalStateHashesRequest getStateHashRequest) { + int fromHeight = getStateHashRequest.getHeight(); + List proposalStateHashes = proposalStateBlockChain.stream() + .filter(e -> e.getHeight() >= fromHeight) + .map(ProposalStateBlock::getMyStateHash) + .collect(Collectors.toList()); + proposalStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), proposalStateHashes); + } + + @Override + public void onPeersStateHashes(List stateHashes, Optional peersNodeAddress) { + AtomicBoolean hasChanged = new AtomicBoolean(false); + stateHashes.forEach(daoStateHash -> { + boolean changed = processPeersProposalStateHash(daoStateHash, peersNodeAddress, false); + if (changed) { + hasChanged.set(true); + } + }); + + if (hasChanged.get()) { + listeners.forEach(Listener::onProposalStateBlockChainChanged); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void requestHashesFromGenesisBlockHeight(String peersAddress) { + proposalStateNetworkService.requestHashes(genesisTxInfo.getGenesisBlockHeight(), peersAddress); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void maybeUpdateHashChain(int blockHeight) { + // We use first block in blind vote phase to create the hash of our proposals. We prefer to wait as long as + // possible to increase the chance that we have received all proposals. + if (!isFirstBlockOfBlindVotePhase(blockHeight)) { + return; + } + + periodService.getCycle(blockHeight).ifPresent(cycle -> { + List proposals = proposalService.getValidatedProposals().stream() + .filter(e -> periodService.isTxInPhaseAndCycle(e.getTxId(), DaoPhase.Phase.PROPOSAL, blockHeight)) + .sorted(Comparator.comparing(Proposal::getTxId)).collect(Collectors.toList()); + + // We use MyProposalList to get the serialized bytes from the proposals list + byte[] serializedProposals = new MyProposalList(proposals).toProtoMessage().toByteArray(); + + byte[] prevHash; + if (proposalStateBlockChain.isEmpty()) { + prevHash = new byte[0]; + } else { + prevHash = proposalStateBlockChain.getLast().getHash(); + } + byte[] combined = ArrayUtils.addAll(prevHash, serializedProposals); + byte[] hash = Hash.getSha256Ripemd160hash(combined); + ProposalStateHash myProposalStateHash = new ProposalStateHash(blockHeight, hash, prevHash, proposals.size()); + ProposalStateBlock proposalStateBlock = new ProposalStateBlock(myProposalStateHash); + proposalStateBlockChain.add(proposalStateBlock); + proposalStateHashChain.add(myProposalStateHash); + + // We only broadcast after parsing of blockchain is complete + if (parseBlockChainComplete) { + // We notify listeners only after batch processing to avoid performance issues at UI code + listeners.forEach(Listener::onProposalStateBlockChainChanged); + + // We delay broadcast to give peers enough time to have received the block. + // Otherwise they would ignore our data if received block is in future to their local blockchain. + int delayInSec = 5 + new Random().nextInt(10); + UserThread.runAfter(() -> proposalStateNetworkService.broadcastMyStateHash(myProposalStateHash), delayInSec); + } + }); + } + + private boolean processPeersProposalStateHash(ProposalStateHash proposalStateHash, Optional peersNodeAddress, boolean notifyListeners) { + AtomicBoolean changed = new AtomicBoolean(false); + AtomicBoolean isInConflict = new AtomicBoolean(this.isInConflict); + StringBuilder sb = new StringBuilder(); + proposalStateBlockChain.stream() + .filter(e -> e.getHeight() == proposalStateHash.getHeight()).findAny() + .ifPresent(daoStateBlock -> { + String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress) + .orElseGet(() -> "Unknown peer " + new Random().nextInt(10000)); + daoStateBlock.putInPeersMap(peersNodeAddressAsString, proposalStateHash); + if (!daoStateBlock.getMyStateHash().hasEqualHash(proposalStateHash)) { + daoStateBlock.putInConflictMap(peersNodeAddressAsString, proposalStateHash); + isInConflict.set(true); + sb.append("We received a block hash from peer ") + .append(peersNodeAddressAsString) + .append(" which conflicts with our block hash.\n") + .append("my proposalStateHash=") + .append(daoStateBlock.getMyStateHash()) + .append("\npeers proposalStateHash=") + .append(proposalStateHash); + } + changed.set(true); + }); + + this.isInConflict = isInConflict.get(); + + String conflictMsg = sb.toString(); + if (this.isInConflict && !conflictMsg.isEmpty()) { + log.warn(conflictMsg); + } + + if (notifyListeners && changed.get()) { + listeners.forEach(Listener::onProposalStateBlockChainChanged); + } + + return changed.get(); + } + + private boolean isFirstBlockOfBlindVotePhase(int blockHeight) { + return blockHeight == periodService.getFirstBlockOfPhase(blockHeight, DaoPhase.Phase.BLIND_VOTE); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateBlock.java b/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateBlock.java new file mode 100644 index 00000000000..bc527eeb6e1 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateBlock.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.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class BlindVoteStateBlock extends StateBlock { + public BlindVoteStateBlock(BlindVoteStateHash myBlindVoteStateHash) { + super(myBlindVoteStateHash); + } + + public int getNumBlindVotes() { + return myStateHash.getNumBlindVotes(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateHash.java b/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateHash.java new file mode 100644 index 00000000000..4ac39908b14 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/BlindVoteStateHash.java @@ -0,0 +1,65 @@ +/* + * 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 io.bisq.generated.protobuffer.PB; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) + +public final class BlindVoteStateHash extends StateHash { + @Getter + private final int numBlindVotes; + + public BlindVoteStateHash(int cycleStartBlockHeight, byte[] hash, byte[] prevHash, int numBlindVotes) { + super(cycleStartBlockHeight, hash, prevHash); + this.numBlindVotes = numBlindVotes; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public PB.BlindVoteStateHash toProtoMessage() { + return PB.BlindVoteStateHash.newBuilder() + .setHeight(height) + .setHash(ByteString.copyFrom(hash)) + .setPrevHash(ByteString.copyFrom(prevHash)) + .setNumBlindVotes(numBlindVotes).build(); + } + + public static BlindVoteStateHash fromProto(PB.BlindVoteStateHash proto) { + return new BlindVoteStateHash(proto.getHeight(), + proto.getHash().toByteArray(), + proto.getPrevHash().toByteArray(), + proto.getNumBlindVotes()); + } + + @Override + public String toString() { + return "BlindVoteStateHash{" + + "\n numBlindVotes=" + numBlindVotes + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateBlock.java b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateBlock.java new file mode 100644 index 00000000000..8bd91e67fa6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateBlock.java @@ -0,0 +1,29 @@ +/* + * 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.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class DaoStateBlock extends StateBlock { + public DaoStateBlock(DaoStateHash myDaoStateHash) { + super(myDaoStateHash); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateHash.java b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateHash.java new file mode 100644 index 00000000000..db5e0c1bc69 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateHash.java @@ -0,0 +1,50 @@ +/* + * 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 io.bisq.generated.protobuffer.PB; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class DaoStateHash extends StateHash { + public DaoStateHash(int height, byte[] hash, byte[] prevHash) { + super(height, hash, prevHash); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public PB.DaoStateHash toProtoMessage() { + return PB.DaoStateHash.newBuilder() + .setHeight(height) + .setHash(ByteString.copyFrom(hash)) + .setPrevHash(ByteString.copyFrom(prevHash)).build(); + } + + public static DaoStateHash fromProto(PB.DaoStateHash proto) { + return new DaoStateHash(proto.getHeight(), + proto.getHash().toByteArray(), + proto.getPrevHash().toByteArray()); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateBlock.java b/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateBlock.java new file mode 100644 index 00000000000..ddfc83dbb67 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateBlock.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.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@EqualsAndHashCode(callSuper = true) +public class ProposalStateBlock extends StateBlock { + public ProposalStateBlock(ProposalStateHash myProposalStateHash) { + super(myProposalStateHash); + } + + public int getNumProposals() { + return myStateHash.getNumProposals(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateHash.java b/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateHash.java new file mode 100644 index 00000000000..147170096d9 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/ProposalStateHash.java @@ -0,0 +1,66 @@ +/* + * 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 io.bisq.generated.protobuffer.PB; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) + +public final class ProposalStateHash extends StateHash { + @Getter + private final int numProposals; + + public ProposalStateHash(int cycleStartBlockHeight, byte[] hash, byte[] prevHash, int numProposals) { + super(cycleStartBlockHeight, hash, prevHash); + this.numProposals = numProposals; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public PB.ProposalStateHash toProtoMessage() { + return PB.ProposalStateHash.newBuilder() + .setHeight(height) + .setHash(ByteString.copyFrom(hash)) + .setPrevHash(ByteString.copyFrom(prevHash)) + .setNumProposals(numProposals).build(); + } + + public static ProposalStateHash fromProto(PB.ProposalStateHash proto) { + return new ProposalStateHash(proto.getHeight(), + proto.getHash().toByteArray(), + proto.getPrevHash().toByteArray(), + proto.getNumProposals()); + } + + + @Override + public String toString() { + return "ProposalStateHash{" + + "\n numProposals=" + numProposals + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/StateBlock.java b/core/src/main/java/bisq/core/dao/monitoring/model/StateBlock.java new file mode 100644 index 00000000000..b41ddc2296f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/StateBlock.java @@ -0,0 +1,71 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * Contains my StateHash at a particular block height and the received stateHash from our peers. + * The maps get updated over time, this is not an immutable class. + */ +@Getter +@EqualsAndHashCode +public abstract class StateBlock { + protected final T myStateHash; + + private final Map peersMap = new HashMap<>(); + private final Map inConflictMap = new HashMap<>(); + + StateBlock(T myStateHash) { + this.myStateHash = myStateHash; + } + + public void putInPeersMap(String peersNodeAddress, T stateHash) { + peersMap.putIfAbsent(peersNodeAddress, stateHash); + } + + public void putInConflictMap(String peersNodeAddress, T stateHash) { + inConflictMap.putIfAbsent(peersNodeAddress, stateHash); + } + + // Delegates + public int getHeight() { + return myStateHash.getHeight(); + } + + public byte[] getHash() { + return myStateHash.getHash(); + } + + public byte[] getPrevHash() { + return myStateHash.getPrevHash(); + } + + @Override + public String toString() { + return "StateBlock{" + + "\n myStateHash=" + myStateHash + + ",\n peersMap=" + peersMap + + ",\n inConflictMap=" + inConflictMap + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/model/StateHash.java b/core/src/main/java/bisq/core/dao/monitoring/model/StateHash.java new file mode 100644 index 00000000000..12cb299a11c --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/model/StateHash.java @@ -0,0 +1,73 @@ +/* + * 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 bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import java.util.Arrays; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Contains the blockHeight, the hash and the previous hash of the state. + * As the hash is created from the state at the particular height including the previous hash we get the history of + * the full chain included and we know if the hash matches at a particular height that all the past blocks need to match + * as well. + */ +@EqualsAndHashCode +@Getter +@Slf4j +public abstract class StateHash implements PersistablePayload, NetworkPayload { + protected final int height; + protected final byte[] hash; + // For first block the prevHash is an empty byte array + protected final byte[] prevHash; + + StateHash(int height, byte[] hash, byte[] prevHash) { + this.height = height; + this.hash = hash; + this.prevHash = prevHash; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean hasEqualHash(StateHash other) { + return Arrays.equals(hash, other.getHash()); + } + + public byte[] getHash() { + return hash; + } + + @Override + public String toString() { + return "StateHash{" + + "\n height=" + height + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + ",\n prevHash=" + Utilities.bytesAsHexString(prevHash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/BlindVoteStateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/BlindVoteStateNetworkService.java new file mode 100644 index 00000000000..81bd46f8ee1 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/BlindVoteStateNetworkService.java @@ -0,0 +1,85 @@ +/* + * 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.network; + +import bisq.core.dao.monitoring.model.BlindVoteStateHash; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BlindVoteStateNetworkService extends StateNetworkService { + @Inject + public BlindVoteStateNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster) { + super(networkNode, peerManager, broadcaster); + } + + @Override + protected GetBlindVoteStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) { + return (GetBlindVoteStateHashesRequest) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetBlindVoteStateHashesRequest; + } + + @Override + protected NewBlindVoteStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return (NewBlindVoteStateHashMessage) networkEnvelope; + } + + @Override + protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof NewBlindVoteStateHashMessage; + } + + @Override + protected GetBlindVoteStateHashesResponse getGetStateHashesResponse(int nonce, List stateHashes) { + return new GetBlindVoteStateHashesResponse(stateHashes, nonce); + } + + @Override + protected NewBlindVoteStateHashMessage getNewStateHashMessage(BlindVoteStateHash myStateHash) { + return new NewBlindVoteStateHashMessage(myStateHash); + } + + @Override + protected RequestBlindVoteStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener listener) { + return new RequestBlindVoteStateHashesHandler(networkNode, peerManager, nodeAddress, listener); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/DaoStateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/DaoStateNetworkService.java new file mode 100644 index 00000000000..e394977b31f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/DaoStateNetworkService.java @@ -0,0 +1,86 @@ +/* + * 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.network; + +import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DaoStateNetworkService extends StateNetworkService { + @Inject + public DaoStateNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster) { + super(networkNode, peerManager, broadcaster); + } + + @Override + protected GetDaoStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) { + return (GetDaoStateHashesRequest) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetDaoStateHashesRequest; + } + + @Override + protected NewDaoStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return (NewDaoStateHashMessage) networkEnvelope; + } + + @Override + protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof NewDaoStateHashMessage; + } + + @Override + protected GetDaoStateHashesResponse getGetStateHashesResponse(int nonce, List stateHashes) { + return new GetDaoStateHashesResponse(stateHashes, nonce); + } + + @Override + protected NewDaoStateHashMessage getNewStateHashMessage(DaoStateHash myStateHash) { + return new NewDaoStateHashMessage(myStateHash); + } + + @Override + protected RequestDaoStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener listener) { + return new RequestDaoStateHashesHandler(networkNode, peerManager, nodeAddress, listener); + } + +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/ProposalStateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/ProposalStateNetworkService.java new file mode 100644 index 00000000000..76e6f33bd18 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/ProposalStateNetworkService.java @@ -0,0 +1,86 @@ +/* + * 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.network; + +import bisq.core.dao.monitoring.model.ProposalStateHash; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ProposalStateNetworkService extends StateNetworkService { + @Inject + public ProposalStateNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster) { + super(networkNode, peerManager, broadcaster); + } + + @Override + protected GetProposalStateHashesRequest castToGetStateHashRequest(NetworkEnvelope networkEnvelope) { + return (GetProposalStateHashesRequest) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetProposalStateHashesRequest; + } + + @Override + protected NewProposalStateHashMessage castToNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return (NewProposalStateHashMessage) networkEnvelope; + } + + @Override + protected boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof NewProposalStateHashMessage; + } + + @Override + protected GetProposalStateHashesResponse getGetStateHashesResponse(int nonce, List stateHashes) { + return new GetProposalStateHashesResponse(stateHashes, nonce); + } + + @Override + protected NewProposalStateHashMessage getNewStateHashMessage(ProposalStateHash myStateHash) { + return new NewProposalStateHashMessage(myStateHash); + } + + @Override + protected RequestProposalStateHashesHandler getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener listener) { + return new RequestProposalStateHashesHandler(networkNode, peerManager, nodeAddress, listener); + } + +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/RequestBlindVoteStateHashesHandler.java b/core/src/main/java/bisq/core/dao/monitoring/network/RequestBlindVoteStateHashesHandler.java new file mode 100644 index 00000000000..5f33b05a487 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/RequestBlindVoteStateHashesHandler.java @@ -0,0 +1,54 @@ +/* + * 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.network; + +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RequestBlindVoteStateHashesHandler extends RequestStateHashesHandler { + RequestBlindVoteStateHashesHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + Listener listener) { + super(networkNode, peerManager, nodeAddress, listener); + } + + @Override + protected GetBlindVoteStateHashesRequest getGetStateHashesRequest(int fromHeight) { + return new GetBlindVoteStateHashesRequest(fromHeight, nonce); + } + + @Override + protected GetBlindVoteStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return (GetBlindVoteStateHashesResponse) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetBlindVoteStateHashesResponse; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/RequestDaoStateHashesHandler.java b/core/src/main/java/bisq/core/dao/monitoring/network/RequestDaoStateHashesHandler.java new file mode 100644 index 00000000000..6a7b7bdcc7f --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/RequestDaoStateHashesHandler.java @@ -0,0 +1,54 @@ +/* + * 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.network; + +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RequestDaoStateHashesHandler extends RequestStateHashesHandler { + RequestDaoStateHashesHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + Listener listener) { + super(networkNode, peerManager, nodeAddress, listener); + } + + @Override + protected GetDaoStateHashesRequest getGetStateHashesRequest(int fromHeight) { + return new GetDaoStateHashesRequest(fromHeight, nonce); + } + + @Override + protected GetDaoStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return (GetDaoStateHashesResponse) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetDaoStateHashesResponse; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/RequestProposalStateHashesHandler.java b/core/src/main/java/bisq/core/dao/monitoring/network/RequestProposalStateHashesHandler.java new file mode 100644 index 00000000000..0e4a20502e0 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/RequestProposalStateHashesHandler.java @@ -0,0 +1,54 @@ +/* + * 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.network; + +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RequestProposalStateHashesHandler extends RequestStateHashesHandler { + RequestProposalStateHashesHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + Listener listener) { + super(networkNode, peerManager, nodeAddress, listener); + } + + @Override + protected GetProposalStateHashesRequest getGetStateHashesRequest(int fromHeight) { + return new GetProposalStateHashesRequest(fromHeight, nonce); + } + + @Override + protected GetProposalStateHashesResponse castToGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return (GetProposalStateHashesResponse) networkEnvelope; + } + + @Override + protected boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope) { + return networkEnvelope instanceof GetProposalStateHashesResponse; + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/RequestStateHashesHandler.java b/core/src/main/java/bisq/core/dao/monitoring/network/RequestStateHashesHandler.java new file mode 100644 index 00000000000..0b806b47403 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/RequestStateHashesHandler.java @@ -0,0 +1,227 @@ +/* + * 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.network; + +import bisq.core.dao.monitoring.network.messages.GetStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetStateHashesResponse; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.proto.network.NetworkEnvelope; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.Optional; +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Slf4j +abstract class RequestStateHashesHandler implements MessageListener { + private static final long TIMEOUT = 120; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onComplete(Res getStateHashesResponse, Optional peersNodeAddress); + + @SuppressWarnings("UnusedParameters") + void onFault(String errorMessage, @SuppressWarnings("SameParameterValue") @Nullable Connection connection); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final NetworkNode networkNode; + private final PeerManager peerManager; + private final NodeAddress nodeAddress; + private final Listener listener; + private Timer timeoutTimer; + final int nonce = new Random().nextInt(); + private boolean stopped; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + RequestStateHashesHandler(NetworkNode networkNode, + PeerManager peerManager, + NodeAddress nodeAddress, + Listener listener) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.nodeAddress = nodeAddress; + this.listener = listener; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract Req getGetStateHashesRequest(int fromHeight); + + protected abstract Res castToGetStateHashesResponse(NetworkEnvelope networkEnvelope); + + protected abstract boolean isGetStateHashesResponse(NetworkEnvelope networkEnvelope); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void requestStateHashes(int fromHeight) { + if (!stopped) { + Req getStateHashesRequest = getGetStateHashesRequest(fromHeight); + if (timeoutTimer == null) { + timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions + if (!stopped) { + String errorMessage = "A timeout occurred at sending getStateHashesRequest:" + getStateHashesRequest + + " on peersNodeAddress:" + nodeAddress; + log.debug(errorMessage + " / RequestStateHashesHandler=" + RequestStateHashesHandler.this); + handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT); + } else { + log.trace("We have stopped already. We ignore that timeoutTimer.run call. " + + "Might be caused by an previous networkNode.sendMessage.onFailure."); + } + }, + TIMEOUT); + } + + log.info("We send to peer {} a {}.", nodeAddress, getStateHashesRequest); + networkNode.addMessageListener(this); + SettableFuture future = networkNode.sendMessage(nodeAddress, getStateHashesRequest); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + if (!stopped) { + log.info("Sending of {} message to peer {} succeeded.", + getStateHashesRequest.getClass().getSimpleName(), + nodeAddress.getFullAddress()); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." + + "Might be caused by an previous timeout."); + } + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending getStateHashesRequest to " + nodeAddress + + " failed. That is expected if the peer is offline.\n\t" + + "getStateHashesRequest=" + getStateHashesRequest + "." + + "\n\tException=" + throwable.getMessage(); + log.error(errorMessage); + handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + + "Might be caused by an previous timeout."); + } + } + }); + } else { + log.warn("We have stopped already. We ignore that requestProposalsHash call."); + } + } + + public void cancel() { + cleanup(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MessageListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (isGetStateHashesResponse(networkEnvelope)) { + if (connection.getPeersNodeAddressOptional().isPresent() && connection.getPeersNodeAddressOptional().get().equals(nodeAddress)) { + if (!stopped) { + Res getStateHashesResponse = castToGetStateHashesResponse(networkEnvelope); + if (getStateHashesResponse.getRequestNonce() == nonce) { + stopTimeoutTimer(); + cleanup(); + log.info("We received from peer {} a {} with {} stateHashes", + nodeAddress.getFullAddress(), getStateHashesResponse.getClass().getSimpleName(), + getStateHashesResponse.getStateHashes().size()); + listener.onComplete(getStateHashesResponse, connection.getPeersNodeAddressOptional()); + } else { + log.warn("Nonce not matching. That can happen rarely if we get a response after a canceled " + + "handshake (timeout causes connection close but peer might have sent a msg before " + + "connection was closed).\n\t" + + "We drop that message. nonce={} / requestNonce={}", + nonce, getStateHashesResponse.getRequestNonce()); + } + } else { + log.warn("We have stopped already."); + } + } else if (connection.getPeersNodeAddressOptional().isPresent()) { + log.debug("{}: We got a message from another node. We ignore that.", + this.getClass().getSimpleName()); + } + } + } + + public void stop() { + cleanup(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("UnusedParameters") + private void handleFault(String errorMessage, NodeAddress nodeAddress, CloseConnectionReason closeConnectionReason) { + cleanup(); + peerManager.handleConnectionFault(nodeAddress); + listener.onFault(errorMessage, null); + } + + private void cleanup() { + stopped = true; + networkNode.removeMessageListener(this); + stopTimeoutTimer(); + } + + private void stopTimeoutTimer() { + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java new file mode 100644 index 00000000000..c6e9f3c0442 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/StateNetworkService.java @@ -0,0 +1,208 @@ +/* + * 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.network; + +import bisq.core.dao.monitoring.model.StateHash; +import bisq.core.dao.monitoring.network.messages.GetStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewStateHashMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.peers.PeerManager; + +import bisq.common.proto.network.NetworkEnvelope; + +import javax.inject.Inject; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class StateNetworkService, + Han extends RequestStateHashesHandler, + StH extends StateHash> implements MessageListener { + + public interface Listener { + void onNewStateHashMessage(Msg newStateHashMessage, Connection connection); + + void onGetStateHashRequest(Connection connection, Req getStateHashRequest); + + void onPeersStateHashes(List stateHashes, Optional peersNodeAddress); + } + + protected final NetworkNode networkNode; + protected final PeerManager peerManager; + private final Broadcaster broadcaster; + + @Getter + private final Map requestStateHashHandlerMap = new HashMap<>(); + private final List> listeners = new CopyOnWriteArrayList<>(); + private boolean messageListenerAdded; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public StateNetworkService(NetworkNode networkNode, + PeerManager peerManager, + Broadcaster broadcaster) { + this.networkNode = networkNode; + this.peerManager = peerManager; + this.broadcaster = broadcaster; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract Req castToGetStateHashRequest(NetworkEnvelope networkEnvelope); + + + protected abstract boolean isGetStateHashesRequest(NetworkEnvelope networkEnvelope); + + + protected abstract Msg castToNewStateHashMessage(NetworkEnvelope networkEnvelope); + + + protected abstract boolean isNewStateHashMessage(NetworkEnvelope networkEnvelope); + + protected abstract Res getGetStateHashesResponse(int nonce, List stateHashes); + + protected abstract Msg getNewStateHashMessage(StH myStateHash); + + protected abstract Han getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener listener); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MessageListener implementation + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (isNewStateHashMessage(networkEnvelope)) { + Msg newStateHashMessage = castToNewStateHashMessage(networkEnvelope); + log.info("We received a {} from peer {} with stateHash={} ", + newStateHashMessage.getClass().getSimpleName(), + connection.getPeersNodeAddressOptional(), + newStateHashMessage.getStateHash()); + listeners.forEach(e -> e.onNewStateHashMessage(newStateHashMessage, connection)); + } else if (isGetStateHashesRequest(networkEnvelope)) { + Req getStateHashRequest = castToGetStateHashRequest(networkEnvelope); + log.info("We received a {} from peer {} for height={} ", + getStateHashRequest.getClass().getSimpleName(), + connection.getPeersNodeAddressOptional(), + getStateHashRequest.getHeight()); + listeners.forEach(e -> e.onGetStateHashRequest(connection, getStateHashRequest)); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListeners() { + if (!messageListenerAdded) { + networkNode.addMessageListener(this); + messageListenerAdded = true; + } + } + + public void sendGetStateHashesResponse(Connection connection, int nonce, List stateHashes) { + Res getStateHashesResponse = getGetStateHashesResponse(nonce, stateHashes); + log.info("Send {} with {} stateHashes to peer {}", getStateHashesResponse.getClass().getSimpleName(), + stateHashes.size(), connection.getPeersNodeAddressOptional()); + connection.sendMessage(getStateHashesResponse); + } + + public void requestHashesFromAllConnectedSeedNodes(int fromHeight) { + networkNode.getConfirmedConnections().stream() + .filter(peerManager::isSeedNode) + .forEach(connection -> connection.getPeersNodeAddressOptional() + .ifPresent(e -> requestHashesFromSeedNode(fromHeight, e))); + } + + public void broadcastMyStateHash(StH myStateHash) { + NewStateHashMessage newStateHashMessage = getNewStateHashMessage(myStateHash); + broadcaster.broadcast(newStateHashMessage, networkNode.getNodeAddress(), null, true); + } + + public void requestHashes(int fromHeight, String peersAddress) { + requestHashesFromSeedNode(fromHeight, new NodeAddress(peersAddress)); + } + + public void reset() { + requestStateHashHandlerMap.clear(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void requestHashesFromSeedNode(int fromHeight, NodeAddress nodeAddress) { + RequestStateHashesHandler.Listener listener = new RequestStateHashesHandler.Listener<>() { + @Override + public void onComplete(Res getStateHashesResponse, Optional peersNodeAddress) { + requestStateHashHandlerMap.remove(nodeAddress); + List stateHashes = getStateHashesResponse.getStateHashes(); + listeners.forEach(e -> e.onPeersStateHashes(stateHashes, peersNodeAddress)); + } + + @Override + public void onFault(String errorMessage, @Nullable Connection connection) { + log.warn("requestDaoStateHashesHandler with outbound connection failed.\n\tnodeAddress={}\n\t" + + "ErrorMessage={}", nodeAddress, errorMessage); + requestStateHashHandlerMap.remove(nodeAddress); + } + }; + Han requestStateHashesHandler = getRequestStateHashesHandler(nodeAddress, listener); + requestStateHashHandlerMap.put(nodeAddress, requestStateHashesHandler); + requestStateHashesHandler.requestStateHashes(fromHeight); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesRequest.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesRequest.java new file mode 100644 index 00000000000..aeb6c417849 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesRequest.java @@ -0,0 +1,56 @@ +/* + * 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.network.messages; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetBlindVoteStateHashesRequest extends GetStateHashesRequest { + public GetBlindVoteStateHashesRequest(int fromCycleStartHeight, int nonce) { + super(fromCycleStartHeight, nonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetBlindVoteStateHashesRequest(int height, int nonce, int messageVersion) { + super(height, nonce, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetBlindVoteStateHashesRequest(PB.GetBlindVoteStateHashesRequest.newBuilder() + .setHeight(height) + .setNonce(nonce)) + .build(); + } + + public static NetworkEnvelope fromProto(PB.GetBlindVoteStateHashesRequest proto, int messageVersion) { + return new GetBlindVoteStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesResponse.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesResponse.java new file mode 100644 index 00000000000..a3e849d2877 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetBlindVoteStateHashesResponse.java @@ -0,0 +1,72 @@ +/* + * 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.network.messages; + +import bisq.core.dao.monitoring.model.BlindVoteStateHash; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetBlindVoteStateHashesResponse extends GetStateHashesResponse { + public GetBlindVoteStateHashesResponse(List stateHashes, int requestNonce) { + super(stateHashes, requestNonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetBlindVoteStateHashesResponse(List stateHashes, + int requestNonce, + int messageVersion) { + super(stateHashes, requestNonce, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetBlindVoteStateHashesResponse(PB.GetBlindVoteStateHashesResponse.newBuilder() + .addAllStateHashes(stateHashes.stream() + .map(BlindVoteStateHash::toProtoMessage) + .collect(Collectors.toList())) + .setRequestNonce(requestNonce)) + .build(); + } + + public static NetworkEnvelope fromProto(PB.GetBlindVoteStateHashesResponse proto, int messageVersion) { + return new GetBlindVoteStateHashesResponse(proto.getStateHashesList().isEmpty() ? + new ArrayList<>() : + proto.getStateHashesList().stream() + .map(BlindVoteStateHash::fromProto) + .collect(Collectors.toList()), + proto.getRequestNonce(), + messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesRequest.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesRequest.java new file mode 100644 index 00000000000..d86ae1ee2f5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesRequest.java @@ -0,0 +1,56 @@ +/* + * 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.network.messages; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetDaoStateHashesRequest extends GetStateHashesRequest { + public GetDaoStateHashesRequest(int height, int nonce) { + super(height, nonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetDaoStateHashesRequest(int height, int nonce, int messageVersion) { + super(height, nonce, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetDaoStateHashesRequest(PB.GetDaoStateHashesRequest.newBuilder() + .setHeight(height) + .setNonce(nonce)) + .build(); + } + + public static NetworkEnvelope fromProto(PB.GetDaoStateHashesRequest proto, int messageVersion) { + return new GetDaoStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesResponse.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesResponse.java new file mode 100644 index 00000000000..83998072927 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetDaoStateHashesResponse.java @@ -0,0 +1,72 @@ +/* + * 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.network.messages; + +import bisq.core.dao.monitoring.model.DaoStateHash; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetDaoStateHashesResponse extends GetStateHashesResponse { + public GetDaoStateHashesResponse(List daoStateHashes, int requestNonce) { + super(daoStateHashes, requestNonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetDaoStateHashesResponse(List daoStateHashes, + int requestNonce, + int messageVersion) { + super(daoStateHashes, requestNonce, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetDaoStateHashesResponse(PB.GetDaoStateHashesResponse.newBuilder() + .addAllStateHashes(stateHashes.stream() + .map(DaoStateHash::toProtoMessage) + .collect(Collectors.toList())) + .setRequestNonce(requestNonce)) + .build(); + } + + public static NetworkEnvelope fromProto(PB.GetDaoStateHashesResponse proto, int messageVersion) { + return new GetDaoStateHashesResponse(proto.getStateHashesList().isEmpty() ? + new ArrayList<>() : + proto.getStateHashesList().stream() + .map(DaoStateHash::fromProto) + .collect(Collectors.toList()), + proto.getRequestNonce(), + messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesRequest.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesRequest.java new file mode 100644 index 00000000000..e32895b7c1d --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesRequest.java @@ -0,0 +1,56 @@ +/* + * 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.network.messages; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetProposalStateHashesRequest extends GetStateHashesRequest { + public GetProposalStateHashesRequest(int fromCycleStartHeight, int nonce) { + super(fromCycleStartHeight, nonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetProposalStateHashesRequest(int height, int nonce, int messageVersion) { + super(height, nonce, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetProposalStateHashesRequest(PB.GetProposalStateHashesRequest.newBuilder() + .setHeight(height) + .setNonce(nonce)) + .build(); + } + + public static NetworkEnvelope fromProto(PB.GetProposalStateHashesRequest proto, int messageVersion) { + return new GetProposalStateHashesRequest(proto.getHeight(), proto.getNonce(), messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesResponse.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesResponse.java new file mode 100644 index 00000000000..485ee25d4ec --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetProposalStateHashesResponse.java @@ -0,0 +1,72 @@ +/* + * 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.network.messages; + +import bisq.core.dao.monitoring.model.ProposalStateHash; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class GetProposalStateHashesResponse extends GetStateHashesResponse { + public GetProposalStateHashesResponse(List proposalStateHashes, int requestNonce) { + super(proposalStateHashes, requestNonce, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private GetProposalStateHashesResponse(List proposalStateHashes, + int requestNonce, + int messageVersion) { + super(proposalStateHashes, requestNonce, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setGetProposalStateHashesResponse(PB.GetProposalStateHashesResponse.newBuilder() + .addAllStateHashes(stateHashes.stream() + .map(ProposalStateHash::toProtoMessage) + .collect(Collectors.toList())) + .setRequestNonce(requestNonce)) + .build(); + } + + public static NetworkEnvelope fromProto(PB.GetProposalStateHashesResponse proto, int messageVersion) { + return new GetProposalStateHashesResponse(proto.getStateHashesList().isEmpty() ? + new ArrayList<>() : + proto.getStateHashesList().stream() + .map(ProposalStateHash::fromProto) + .collect(Collectors.toList()), + proto.getRequestNonce(), + messageVersion); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesRequest.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesRequest.java new file mode 100644 index 00000000000..9db856ad662 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesRequest.java @@ -0,0 +1,54 @@ +/* + * 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.network.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.proto.network.NetworkEnvelope; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public abstract class GetStateHashesRequest extends NetworkEnvelope implements DirectMessage, CapabilityRequiringPayload { + protected final int height; + protected final int nonce; + + protected GetStateHashesRequest(int height, int nonce, int messageVersion) { + super(messageVersion); + this.height = height; + this.nonce = nonce; + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } + + @Override + public String toString() { + return "GetStateHashesRequest{" + + ",\n height=" + height + + ",\n nonce=" + nonce + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesResponse.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesResponse.java new file mode 100644 index 00000000000..4dc3e9e66a2 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/GetStateHashesResponse.java @@ -0,0 +1,53 @@ +/* + * 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.network.messages; + +import bisq.core.dao.monitoring.model.StateHash; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.ExtendedDataSizePermission; + +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.List; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public abstract class GetStateHashesResponse extends NetworkEnvelope implements DirectMessage, ExtendedDataSizePermission { + protected final List stateHashes; + protected final int requestNonce; + + protected GetStateHashesResponse(List stateHashes, + int requestNonce, + int messageVersion) { + super(messageVersion); + this.stateHashes = stateHashes; + this.requestNonce = requestNonce; + } + + @Override + public String toString() { + return "GetStateHashesResponse{" + + "\n stateHashes=" + stateHashes + + ",\n requestNonce=" + requestNonce + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewBlindVoteStateHashMessage.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewBlindVoteStateHashMessage.java new file mode 100644 index 00000000000..8e4449b6c60 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewBlindVoteStateHashMessage.java @@ -0,0 +1,64 @@ +/* + * 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.network.messages; + +import bisq.core.dao.monitoring.model.BlindVoteStateHash; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class NewBlindVoteStateHashMessage extends NewStateHashMessage { + public NewBlindVoteStateHashMessage(BlindVoteStateHash stateHash) { + super(stateHash, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private NewBlindVoteStateHashMessage(BlindVoteStateHash stateHash, int messageVersion) { + super(stateHash, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setNewBlindVoteStateHashMessage(PB.NewBlindVoteStateHashMessage.newBuilder() + .setStateHash(stateHash.toProtoMessage())) + .build(); + } + + public static NetworkEnvelope fromProto(PB.NewBlindVoteStateHashMessage proto, int messageVersion) { + return new NewBlindVoteStateHashMessage(BlindVoteStateHash.fromProto(proto.getStateHash()), messageVersion); + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewDaoStateHashMessage.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewDaoStateHashMessage.java new file mode 100644 index 00000000000..5300031865b --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewDaoStateHashMessage.java @@ -0,0 +1,64 @@ +/* + * 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.network.messages; + +import bisq.core.dao.monitoring.model.DaoStateHash; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class NewDaoStateHashMessage extends NewStateHashMessage { + public NewDaoStateHashMessage(DaoStateHash daoStateHash) { + super(daoStateHash, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private NewDaoStateHashMessage(DaoStateHash daoStateHash, int messageVersion) { + super(daoStateHash, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setNewDaoStateHashMessage(PB.NewDaoStateHashMessage.newBuilder() + .setStateHash(stateHash.toProtoMessage())) + .build(); + } + + public static NetworkEnvelope fromProto(PB.NewDaoStateHashMessage proto, int messageVersion) { + return new NewDaoStateHashMessage(DaoStateHash.fromProto(proto.getStateHash()), messageVersion); + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewProposalStateHashMessage.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewProposalStateHashMessage.java new file mode 100644 index 00000000000..4e9485fdffd --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewProposalStateHashMessage.java @@ -0,0 +1,64 @@ +/* + * 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.network.messages; + +import bisq.core.dao.monitoring.model.ProposalStateHash; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import io.bisq.generated.protobuffer.PB; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public final class NewProposalStateHashMessage extends NewStateHashMessage { + public NewProposalStateHashMessage(ProposalStateHash proposalStateHash) { + super(proposalStateHash, Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private NewProposalStateHashMessage(ProposalStateHash proposalStateHash, int messageVersion) { + super(proposalStateHash, messageVersion); + } + + @Override + public PB.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setNewProposalStateHashMessage(PB.NewProposalStateHashMessage.newBuilder() + .setStateHash(stateHash.toProtoMessage())) + .build(); + } + + public static NetworkEnvelope fromProto(PB.NewProposalStateHashMessage proto, int messageVersion) { + return new NewProposalStateHashMessage(ProposalStateHash.fromProto(proto.getStateHash()), messageVersion); + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } +} diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewStateHashMessage.java b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewStateHashMessage.java new file mode 100644 index 00000000000..4d2f05d2357 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/messages/NewStateHashMessage.java @@ -0,0 +1,52 @@ +/* + * 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.network.messages; + +import bisq.core.dao.monitoring.model.StateHash; + +import bisq.network.p2p.storage.messages.BroadcastMessage; +import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; + +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode(callSuper = true) +@Getter +public abstract class NewStateHashMessage extends BroadcastMessage implements CapabilityRequiringPayload { + protected final T stateHash; + + protected NewStateHashMessage(T stateHash, int messageVersion) { + super(messageVersion); + this.stateHash = stateHash; + } + + @Override + public Capabilities getRequiredCapabilities() { + return new Capabilities(Capability.DAO_STATE); + } + + @Override + public String toString() { + return "NewStateHashMessage{" + + "\n stateHash=" + stateHash + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/full/FullNode.java b/core/src/main/java/bisq/core/dao/node/full/FullNode.java index 4d84494b758..82db3d23acc 100644 --- a/core/src/main/java/bisq/core/dao/node/full/FullNode.java +++ b/core/src/main/java/bisq/core/dao/node/full/FullNode.java @@ -185,7 +185,9 @@ private void parseBlocksIfNewBlockAvailable(int chainHeight) { } else { log.info("parseBlocksIfNewBlockAvailable did not result in a new block, so we complete."); log.info("parse {} blocks took {} seconds", blocksToParseInBatch, (System.currentTimeMillis() - parseInBatchStartTime) / 1000d); - onParseBlockChainComplete(); + if (!parseBlockchainComplete) { + onParseBlockChainComplete(); + } } }, this::handleError); 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 89d52c1b36c..76f44813ca6 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 @@ -74,7 +74,6 @@ public class RpcService { private final String rpcPassword; private final String rpcPort; private final String rpcBlockPort; - private final boolean dumpBlockchainData; private BtcdClient client; private BtcdDaemon daemon; @@ -92,8 +91,7 @@ public class RpcService { @Inject public RpcService(Preferences preferences, @Named(DaoOptionKeys.RPC_PORT) String rpcPort, - @Named(DaoOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) String rpcBlockPort, - @Named(DaoOptionKeys.DUMP_BLOCKCHAIN_DATA) boolean dumpBlockchainData) { + @Named(DaoOptionKeys.RPC_BLOCK_NOTIFICATION_PORT) String rpcBlockPort) { this.rpcUser = preferences.getRpcUser(); this.rpcPassword = preferences.getRpcPw(); @@ -109,8 +107,6 @@ public RpcService(Preferences preferences, "18443"; // regtest this.rpcBlockPort = rpcBlockPort != null && !rpcBlockPort.isEmpty() ? rpcBlockPort : "5125"; - this.dumpBlockchainData = dumpBlockchainData; - log.info("Version of btcd-cli4j library: {}", BtcdCli4jVersion.VERSION); } @@ -305,7 +301,7 @@ private RawTx getTxFromRawTransaction(RawTransaction rawBtcTx, com.neemre.btcdcl // We don't support raw MS which are the only case where scriptPubKey.getAddresses()>1 String address = scriptPubKey.getAddresses() != null && scriptPubKey.getAddresses().size() == 1 ? scriptPubKey.getAddresses().get(0) : null; - PubKeyScript pubKeyScript = dumpBlockchainData ? new PubKeyScript(scriptPubKey) : null; + PubKeyScript pubKeyScript = new PubKeyScript(scriptPubKey); return new RawTxOutput(rawBtcTxOutput.getN(), rawBtcTxOutput.getValue().movePointRight(8).longValue(), rawBtcTx.getTxId(), diff --git a/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java b/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java index 638f34e78ec..13dcdb467f8 100644 --- a/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java +++ b/core/src/main/java/bisq/core/dao/node/lite/network/RequestBlocksHandler.java @@ -125,7 +125,7 @@ public void requestBlocks() { TIMEOUT); } - log.info("We send to peer {} a {}.", nodeAddress, getBlocksRequest); + log.info("We request blocks from peer {} from block height {}.", nodeAddress, getBlocksRequest.getFromBlockHeight()); networkNode.addMessageListener(this); SettableFuture future = networkNode.sendMessage(nodeAddress, getBlocksRequest); Futures.addCallback(future, new FutureCallback() { @@ -190,7 +190,7 @@ public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { log.warn("We have stopped already. We ignore that onDataRequest call."); } } else { - log.warn("We got a message from another connection and ignore it. That should never happen."); + log.warn("We got a message from ourselves. That should never happen."); } } } diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateListener.java b/core/src/main/java/bisq/core/dao/state/DaoStateListener.java index 5918b917098..02b19da71ac 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateListener.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateListener.java @@ -32,4 +32,9 @@ default void onParseBlockComplete(Block block) { default void onParseBlockCompleteAfterBatchProcessing(Block block) { } + + // Called after the parsing of a block is complete and we do not allow any change in the daoState until the next + // block arrives. + default void onDaoStateChanged(Block block) { + } } 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 fdc18986ab6..4be77315198 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -46,9 +46,9 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -71,6 +71,7 @@ public class DaoStateService implements DaoSetupService { private final List daoStateListeners = new CopyOnWriteArrayList<>(); @Getter private boolean parseBlockChainComplete; + private boolean allowDaoStateChange; /////////////////////////////////////////////////////////////////////////////////////////// @@ -95,6 +96,8 @@ public void addListeners() { @Override public void start() { + allowDaoStateChange = true; + assertDaoStateChange(); daoState.setChainHeight(genesisTxInfo.getGenesisBlockHeight()); } @@ -104,6 +107,9 @@ public void start() { /////////////////////////////////////////////////////////////////////////////////////////// public void applySnapshot(DaoState snapshot) { + allowDaoStateChange = true; + assertDaoStateChange(); + log.info("Apply snapshot with chain height {}", snapshot.getChainHeight()); daoState.setChainHeight(snapshot.getChainHeight()); @@ -117,15 +123,18 @@ public void applySnapshot(DaoState snapshot) { daoState.getUnspentTxOutputMap().clear(); daoState.getUnspentTxOutputMap().putAll(snapshot.getUnspentTxOutputMap()); + daoState.getNonBsqTxOutputMap().clear(); + daoState.getNonBsqTxOutputMap().putAll(snapshot.getNonBsqTxOutputMap()); + + daoState.getSpentInfoMap().clear(); + daoState.getSpentInfoMap().putAll(snapshot.getSpentInfoMap()); + daoState.getConfiscatedLockupTxList().clear(); daoState.getConfiscatedLockupTxList().addAll(snapshot.getConfiscatedLockupTxList()); daoState.getIssuanceMap().clear(); daoState.getIssuanceMap().putAll(snapshot.getIssuanceMap()); - daoState.getSpentInfoMap().clear(); - daoState.getSpentInfoMap().putAll(snapshot.getSpentInfoMap()); - daoState.getParamChangeList().clear(); daoState.getParamChangeList().addAll(snapshot.getParamChangeList()); @@ -144,6 +153,10 @@ DaoState getClone(DaoState snapshotCandidate) { return DaoState.getClone(snapshotCandidate); } + public byte[] getSerializedDaoState() { + return daoState.toProtoMessage().toByteArray(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // ChainHeight @@ -162,6 +175,11 @@ public LinkedList getCycles() { return daoState.getCycles(); } + public void addCycle(Cycle cycle) { + assertDaoStateChange(); + getCycles().add(cycle); + } + @Nullable public Cycle getCurrentCycle() { return !getCycles().isEmpty() ? getCycles().getLast() : null; @@ -190,12 +208,14 @@ public Optional getStartHeightOfNextCycle(int blockHeight) { // First we get the blockHeight set public void onNewBlockHeight(int blockHeight) { + allowDaoStateChange = true; daoState.setChainHeight(blockHeight); daoStateListeners.forEach(listener -> listener.onNewBlockHeight(blockHeight)); } // Second we get the block added with empty txs public void onNewBlockWithEmptyTxs(Block block) { + assertDaoStateChange(); if (daoState.getBlocks().isEmpty() && block.getHeight() != getGenesisBlockHeight()) { log.warn("We don't have any blocks yet and we received a block which is not the genesis block. " + "We ignore that block as the first block need to be the genesis block. " + @@ -215,14 +235,19 @@ public void onParseBlockComplete(Block block) { // VoteResult and other listeners like balances usually listen on onParseTxsCompleteAfterBatchProcessing // so we need to make sure that vote result calculation is completed before (e.g. for comp. request to // update balance). - // TODO the dependency on ordering is nto good here.... Listeners should not depend on order of execution. daoStateListeners.forEach(l -> l.onParseBlockComplete(block)); // We use 2 different handlers as we don't want to update domain listeners during batch processing of all // blocks as that cause performance issues. In earlier versions when we updated at each block it took // 50 sec. for 4000 blocks, after that change it was about 4 sec. + // Clients if (parseBlockChainComplete) daoStateListeners.forEach(l -> l.onParseBlockCompleteAfterBatchProcessing(block)); + + // Here listeners must not trigger any state change in the DAO as we trigger the validation service to + // generate a hash of the state. + allowDaoStateChange = false; + daoStateListeners.forEach(l -> l.onDaoStateChanged(block)); } // Called after parsing of all pending blocks is completed @@ -321,8 +346,8 @@ public Stream getTxStream() { .flatMap(block -> block.getTxs().stream()); } - public Map getTxMap() { - return getTxStream().collect(Collectors.toMap(Tx::getId, tx -> tx)); + public TreeMap getTxMap() { + return new TreeMap<>(getTxStream().collect(Collectors.toMap(Tx::getId, tx -> tx))); } public Set getTxs() { @@ -405,15 +430,17 @@ public Optional getTxOutput(TxOutputKey txOutputKey) { // UnspentTxOutput /////////////////////////////////////////////////////////////////////////////////////////// - public Map getUnspentTxOutputMap() { + public TreeMap getUnspentTxOutputMap() { return daoState.getUnspentTxOutputMap(); } public void addUnspentTxOutput(TxOutput txOutput) { + assertDaoStateChange(); getUnspentTxOutputMap().put(txOutput.getKey(), txOutput); } public void removeUnspentTxOutput(TxOutput txOutput) { + assertDaoStateChange(); getUnspentTxOutputMap().remove(txOutput.getKey()); } @@ -547,6 +574,7 @@ public Set getIssuanceCandidateTxOutputs() { /////////////////////////////////////////////////////////////////////////////////////////// public void addIssuance(Issuance issuance) { + assertDaoStateChange(); daoState.getIssuanceMap().put(issuance.getTxId(), issuance); } @@ -595,16 +623,18 @@ public long getTotalIssuedAmount(IssuanceType issuanceType) { // Non-BSQ /////////////////////////////////////////////////////////////////////////////////////////// + //TODO we never remove NonBsqTxOutput! + //FIXME called at result phase even if there is not a new one (passed txo from prev. cycle which was already added) public void addNonBsqTxOutput(TxOutput txOutput) { + assertDaoStateChange(); checkArgument(txOutput.getTxOutputType() == TxOutputType.ISSUANCE_CANDIDATE_OUTPUT, "txOutput must be type ISSUANCE_CANDIDATE_OUTPUT"); - log.info("addNonBsqTxOutput: txOutput={}", txOutput); daoState.getNonBsqTxOutputMap().put(txOutput.getKey(), txOutput); } public Optional getBtcTxOutput(TxOutputKey key) { // Issuance candidates which did not got accepted in voting are covered here - Map nonBsqTxOutputMap = daoState.getNonBsqTxOutputMap(); + TreeMap nonBsqTxOutputMap = daoState.getNonBsqTxOutputMap(); if (nonBsqTxOutputMap.containsKey(key)) return Optional.of(nonBsqTxOutputMap.get(key)); @@ -827,6 +857,7 @@ public void confiscateBond(String lockupTxId) { } private void doConfiscateBond(String lockupTxId) { + assertDaoStateChange(); log.warn("TxId {} added to confiscatedLockupTxIdList.", lockupTxId); daoState.getConfiscatedLockupTxList().add(lockupTxId); } @@ -855,6 +886,7 @@ public boolean isConfiscatedUnlockTxOutput(String unlockTxId) { /////////////////////////////////////////////////////////////////////////////////////////// public void setNewParam(int blockHeight, Param param, String paramValue) { + assertDaoStateChange(); List paramChangeList = daoState.getParamChangeList(); getStartHeightOfNextCycle(blockHeight) .ifPresent(heightOfNewCycle -> { @@ -903,6 +935,7 @@ public String getParamValue(Param param, int blockHeight) { /////////////////////////////////////////////////////////////////////////////////////////// public void setSpentInfo(TxOutputKey txOutputKey, SpentInfo spentInfo) { + assertDaoStateChange(); daoState.getSpentInfoMap().put(txOutputKey, spentInfo); } @@ -920,9 +953,14 @@ public List getEvaluatedProposalList() { } public void addEvaluatedProposalSet(Set evaluatedProposals) { + assertDaoStateChange(); + evaluatedProposals.stream() .filter(e -> !daoState.getEvaluatedProposalList().contains(e)) .forEach(daoState.getEvaluatedProposalList()::add); + + // We need deterministic order for the hash chain + daoState.getEvaluatedProposalList().sort(Comparator.comparing(EvaluatedProposal::getProposalTxId)); } public List getDecryptedBallotsWithMeritsList() { @@ -930,9 +968,14 @@ public List getDecryptedBallotsWithMeritsList() { } public void addDecryptedBallotsWithMeritsSet(Set decryptedBallotsWithMeritsSet) { + assertDaoStateChange(); + decryptedBallotsWithMeritsSet.stream() .filter(e -> !daoState.getDecryptedBallotsWithMeritsList().contains(e)) .forEach(daoState.getDecryptedBallotsWithMeritsList()::add); + + // We need deterministic order for the hash chain + daoState.getDecryptedBallotsWithMeritsList().sort(Comparator.comparing(DecryptedBallotsWithMerits::getBlindVoteTxId)); } @@ -964,5 +1007,24 @@ public void addDaoStateListener(DaoStateListener listener) { public void removeDaoStateListener(DaoStateListener listener) { daoStateListeners.remove(listener); } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + public String daoStateToString() { + return daoState.toString(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void assertDaoStateChange() { + if (!allowDaoStateChange) + throw new RuntimeException("We got a call which would change the daoState outside of the allowed event phase"); + } } diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java b/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java index 1302ef28bbe..791605d6553 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java @@ -18,6 +18,8 @@ package bisq.core.dao.state; import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.model.DaoStateHash; import bisq.core.dao.state.model.DaoState; import bisq.core.dao.state.model.blockchain.Block; @@ -37,17 +39,20 @@ * SNAPSHOT_GRID old not less than 2 times the SNAPSHOT_GRID old. */ @Slf4j -public class DaoStateSnapshotService implements DaoStateListener { +public class DaoStateSnapshotService { private static final int SNAPSHOT_GRID = 20; private final DaoStateService daoStateService; private final GenesisTxInfo genesisTxInfo; private final CycleService cycleService; private final DaoStateStorageService daoStateStorageService; + private final DaoStateMonitoringService daoStateMonitoringService; - private DaoState snapshotCandidate; + private DaoState daoStateSnapshotCandidate; + private LinkedList daoStateHashChainSnapshotCandidate = new LinkedList<>(); private int chainHeightOfLastApplySnapshot; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @@ -56,79 +61,80 @@ public class DaoStateSnapshotService implements DaoStateListener { public DaoStateSnapshotService(DaoStateService daoStateService, GenesisTxInfo genesisTxInfo, CycleService cycleService, - DaoStateStorageService daoStateStorageService) { + DaoStateStorageService daoStateStorageService, + DaoStateMonitoringService daoStateMonitoringService) { this.daoStateService = daoStateService; this.genesisTxInfo = genesisTxInfo; this.cycleService = cycleService; this.daoStateStorageService = daoStateStorageService; - - this.daoStateService.addDaoStateListener(this); + this.daoStateMonitoringService = daoStateMonitoringService; } /////////////////////////////////////////////////////////////////////////////////////////// - // DaoStateListener + // API /////////////////////////////////////////////////////////////////////////////////////////// - // We listen to each ParseTxsComplete event even if the batch processing of all blocks at startup is not completed - // as we need to write snapshots during that batch processing. - @Override - public void onParseBlockComplete(Block block) { + // We do not use DaoStateListener.onDaoStateChanged but let the DaoEventCoordinator call maybeCreateSnapshot to ensure the + // correct order of execution. + // We need to process during batch processing as well to write snapshots during that process. + public void maybeCreateSnapshot(Block block) { int chainHeight = block.getHeight(); // Either we don't have a snapshot candidate yet, or if we have one the height at that snapshot candidate must be // different to our current height. - boolean noSnapshotCandidateOrDifferentHeight = snapshotCandidate == null || snapshotCandidate.getChainHeight() != chainHeight; + boolean noSnapshotCandidateOrDifferentHeight = daoStateSnapshotCandidate == null || + daoStateSnapshotCandidate.getChainHeight() != chainHeight; if (isSnapshotHeight(chainHeight) && !daoStateService.getBlocks().isEmpty() && isValidHeight(daoStateService.getBlocks().getLast().getHeight()) && noSnapshotCandidateOrDifferentHeight) { // At trigger event we store the latest snapshotCandidate to disc - if (snapshotCandidate != null) { + if (daoStateSnapshotCandidate != null) { // We clone because storage is in a threaded context and we set the snapshotCandidate to our current // state in the next step - DaoState cloned = daoStateService.getClone(snapshotCandidate); - daoStateStorageService.persist(cloned); + DaoState clonedDaoState = daoStateService.getClone(daoStateSnapshotCandidate); + LinkedList clonedDaoStateHashChain = new LinkedList<>(daoStateHashChainSnapshotCandidate); + daoStateStorageService.persist(clonedDaoState, clonedDaoStateHashChain); + log.info("Saved snapshotCandidate with height {} to Disc at height {} ", - snapshotCandidate.getChainHeight(), chainHeight); + daoStateSnapshotCandidate.getChainHeight(), chainHeight); } // Now we clone and keep it in memory for the next trigger event - snapshotCandidate = daoStateService.getClone(); + daoStateSnapshotCandidate = daoStateService.getClone(); + daoStateHashChainSnapshotCandidate = new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain()); + log.info("Cloned new snapshotCandidate at height " + chainHeight); } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// - public void applySnapshot(boolean fromReorg) { - DaoState persisted = daoStateStorageService.getPersistedBsqState(); - if (persisted != null) { - LinkedList blocks = persisted.getBlocks(); - int chainHeightOfPersisted = persisted.getChainHeight(); + DaoState persistedBsqState = daoStateStorageService.getPersistedBsqState(); + LinkedList persistedDaoStateHashChain = daoStateStorageService.getPersistedDaoStateHashChain(); + if (persistedBsqState != null) { + LinkedList blocks = persistedBsqState.getBlocks(); + int chainHeightOfPersisted = persistedBsqState.getChainHeight(); if (!blocks.isEmpty()) { int heightOfLastBlock = blocks.getLast().getHeight(); - log.info("applySnapshot from persisted daoState with height of last block {}", heightOfLastBlock); + log.info("applySnapshot from persistedBsqState daoState with height of last block {}", heightOfLastBlock); if (isValidHeight(heightOfLastBlock)) { if (chainHeightOfLastApplySnapshot != chainHeightOfPersisted) { chainHeightOfLastApplySnapshot = chainHeightOfPersisted; - daoStateService.applySnapshot(persisted); + daoStateService.applySnapshot(persistedBsqState); + daoStateMonitoringService.applySnapshot(persistedDaoStateHashChain); } else { // The reorg might have been caused by the previous parsing which might contains a range of // blocks. log.warn("We applied already a snapshot with chainHeight {}. We will reset the daoState and " + "start over from the genesis transaction again.", chainHeightOfLastApplySnapshot); - persisted = new DaoState(); - applyEmptySnapshot(persisted); + applyEmptySnapshot(); } } } else if (fromReorg) { log.info("We got a reorg and we want to apply the snapshot but it is empty. That is expected in the first blocks until the " + "first snapshot has been created. We use our applySnapshot method and restart from the genesis tx"); - applyEmptySnapshot(persisted); + applyEmptySnapshot(); } } else { log.info("Try to apply snapshot but no stored snapshot available. That is expected at first blocks."); @@ -144,13 +150,16 @@ private boolean isValidHeight(int heightOfLastBlock) { return heightOfLastBlock >= genesisTxInfo.getGenesisBlockHeight(); } - private void applyEmptySnapshot(DaoState persisted) { + private void applyEmptySnapshot() { + DaoState emptyDaoState = new DaoState(); int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight(); - persisted.setChainHeight(genesisBlockHeight); + emptyDaoState.setChainHeight(genesisBlockHeight); chainHeightOfLastApplySnapshot = genesisBlockHeight; - daoStateService.applySnapshot(persisted); + daoStateService.applySnapshot(emptyDaoState); // In case we apply an empty snapshot we need to trigger the cycleService.addFirstCycle method cycleService.addFirstCycle(); + + daoStateMonitoringService.applySnapshot(new LinkedList<>()); } @VisibleForTesting diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java b/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java index 97b334f7612..cbeeb4b08da 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java @@ -17,6 +17,8 @@ package bisq.core.dao.state; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.model.DaoStateHash; import bisq.core.dao.state.model.DaoState; import bisq.network.p2p.storage.persistence.ResourceDataStoreService; @@ -30,6 +32,7 @@ import java.io.File; +import java.util.LinkedList; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; @@ -39,9 +42,13 @@ */ @Slf4j public class DaoStateStorageService extends StoreService { - private static final String FILE_NAME = "DaoStateStore"; + //TODO We need to rename as we have a new file structure with the hashchain feature and need to enforce the + // new file to be used. + // We can rename to DaoStateStore before mainnet launch again. + private static final String FILE_NAME = "DaoStateStore2"; - private DaoState daoState; + private final DaoState daoState; + private final DaoStateMonitoringService daoStateMonitoringService; /////////////////////////////////////////////////////////////////////////////////////////// @@ -51,10 +58,12 @@ public class DaoStateStorageService extends StoreService { @Inject public DaoStateStorageService(ResourceDataStoreService resourceDataStoreService, DaoState daoState, + DaoStateMonitoringService daoStateMonitoringService, @Named(Storage.STORAGE_DIR) File storageDir, Storage daoSnapshotStorage) { super(storageDir, daoSnapshotStorage); this.daoState = daoState; + this.daoStateMonitoringService = daoStateMonitoringService; resourceDataStoreService.addService(this); } @@ -69,12 +78,13 @@ public String getFileName() { return FILE_NAME; } - public void persist(DaoState daoState) { - persist(daoState, 200); + public void persist(DaoState daoState, LinkedList daoStateHashChain) { + persist(daoState, daoStateHashChain, 200); } - public void persist(DaoState daoState, long delayInMilli) { + private void persist(DaoState daoState, LinkedList daoStateHashChain, long delayInMilli) { store.setDaoState(daoState); + store.setDaoStateHashChain(daoStateHashChain); storage.queueUpForSave(store, delayInMilli); } @@ -82,8 +92,12 @@ public DaoState getPersistedBsqState() { return store.getDaoState(); } + public LinkedList getPersistedDaoStateHashChain() { + return store.getDaoStateHashChain(); + } + public void resetDaoState(Runnable resultHandler) { - persist(new DaoState(), 1); + persist(new DaoState(), new LinkedList<>(), 1); UserThread.runAfter(resultHandler, 300, TimeUnit.MILLISECONDS); } @@ -94,6 +108,6 @@ public void resetDaoState(Runnable resultHandler) { @Override protected DaoStateStore createStore() { - return new DaoStateStore(DaoState.getClone(daoState)); + return new DaoStateStore(DaoState.getClone(daoState), new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain())); } } diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateStore.java b/core/src/main/java/bisq/core/dao/state/DaoStateStore.java index b6080a28a3b..300be7f1b81 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateStore.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateStore.java @@ -17,6 +17,7 @@ package bisq.core.dao.state; +import bisq.core.dao.monitoring.model.DaoStateHash; import bisq.core.dao.state.model.DaoState; import bisq.common.proto.persistable.PersistableEnvelope; @@ -25,12 +26,13 @@ import com.google.protobuf.Message; +import java.util.LinkedList; +import java.util.stream.Collectors; + import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import javax.annotation.Nullable; - import static com.google.common.base.Preconditions.checkNotNull; @@ -38,13 +40,16 @@ public class DaoStateStore implements PersistableEnvelope { // DaoState is always a clone and must not be used for read access beside initial read from disc when we apply // the snapshot! - @Nullable @Getter @Setter - DaoState daoState; + private DaoState daoState; + @Getter + @Setter + private LinkedList daoStateHashChain; - DaoStateStore(DaoState daoState) { + DaoStateStore(DaoState daoState, LinkedList daoStateHashChain) { this.daoState = daoState; + this.daoStateHashChain = daoStateHashChain; } @@ -55,13 +60,21 @@ public class DaoStateStore implements PersistableEnvelope { public Message toProtoMessage() { checkNotNull(daoState, "daoState must not be null when toProtoMessage is invoked"); PB.DaoStateStore.Builder builder = PB.DaoStateStore.newBuilder() - .setBsqState(daoState.getBsqStateBuilder()); + .setBsqState(daoState.getBsqStateBuilder()) + .addAllDaoStateHash(daoStateHashChain.stream() + .map(DaoStateHash::toProtoMessage) + .collect(Collectors.toList())); return PB.PersistableEnvelope.newBuilder() .setDaoStateStore(builder) .build(); } public static PersistableEnvelope fromProto(PB.DaoStateStore proto) { - return new DaoStateStore(DaoState.fromProto(proto.getBsqState())); + LinkedList daoStateHashList = proto.getDaoStateHashList().isEmpty() ? + new LinkedList<>() : + new LinkedList<>(proto.getDaoStateHashList().stream() + .map(DaoStateHash::fromProto) + .collect(Collectors.toList())); + return new DaoStateStore(DaoState.fromProto(proto.getBsqState()), daoStateHashList); } } diff --git a/core/src/main/java/bisq/core/dao/state/model/DaoState.java b/core/src/main/java/bisq/core/dao/state/model/DaoState.java index 9e2e406d983..bbe330034ea 100644 --- a/core/src/main/java/bisq/core/dao/state/model/DaoState.java +++ b/core/src/main/java/bisq/core/dao/state/model/DaoState.java @@ -36,10 +36,10 @@ import javax.inject.Inject; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.TreeMap; import java.util.stream.Collectors; import lombok.Getter; @@ -51,6 +51,9 @@ * Holds both blockchain data as well as data derived from the governance process (voting). *

* One BSQ block with empty txs adds 152 bytes which results in about 8 MB/year + * + * For supporting the hashChain we need to ensure deterministic sorting behaviour of all collections so we use a + * TreeMap which is sorted by the key. */ @Slf4j public class DaoState implements PersistablePayload { @@ -77,17 +80,17 @@ public static DaoState getClone(DaoState daoState) { // These maps represent mutual data which can get changed at parsing a transaction @Getter - private final Map unspentTxOutputMap; + private final TreeMap unspentTxOutputMap; @Getter - private final Map nonBsqTxOutputMap; + private final TreeMap nonBsqTxOutputMap; @Getter - private final Map spentInfoMap; + private final TreeMap spentInfoMap; // These maps are related to state change triggered by voting @Getter private final List confiscatedLockupTxList; @Getter - private final Map issuanceMap; // key is txId + private final TreeMap issuanceMap; // key is txId @Getter private final List paramChangeList; @@ -109,11 +112,11 @@ public DaoState() { this(0, new LinkedList<>(), new LinkedList<>(), - new HashMap<>(), - new HashMap<>(), - new HashMap<>(), + new TreeMap<>(), + new TreeMap<>(), + new TreeMap<>(), new ArrayList<>(), - new HashMap<>(), + new TreeMap<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>() @@ -128,11 +131,11 @@ public DaoState() { private DaoState(int chainHeight, LinkedList blocks, LinkedList cycles, - Map unspentTxOutputMap, - Map nonBsqTxOutputMap, - Map spentInfoMap, + TreeMap unspentTxOutputMap, + TreeMap nonBsqTxOutputMap, + TreeMap spentInfoMap, List confiscatedLockupTxList, - Map issuanceMap, + TreeMap issuanceMap, List paramChangeList, List evaluatedProposalList, List decryptedBallotsWithMeritsList) { @@ -182,15 +185,15 @@ public static DaoState fromProto(PB.BsqState proto) { .collect(Collectors.toCollection(LinkedList::new)); LinkedList cycles = proto.getCyclesList().stream() .map(Cycle::fromProto).collect(Collectors.toCollection(LinkedList::new)); - Map unspentTxOutputMap = proto.getUnspentTxOutputMapMap().entrySet().stream() - .collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue()))); - Map nonBsqTxOutputMap = proto.getNonBsqTxOutputMapMap().entrySet().stream() - .collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue()))); - Map spentInfoMap = proto.getSpentInfoMapMap().entrySet().stream() - .collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> SpentInfo.fromProto(e.getValue()))); + TreeMap unspentTxOutputMap = new TreeMap<>(proto.getUnspentTxOutputMapMap().entrySet().stream() + .collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue())))); + TreeMap nonBsqTxOutputMap = new TreeMap<>(proto.getNonBsqTxOutputMapMap().entrySet().stream() + .collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> TxOutput.fromProto(e.getValue())))); + TreeMap spentInfoMap = new TreeMap<>(proto.getSpentInfoMapMap().entrySet().stream() + .collect(Collectors.toMap(e -> TxOutputKey.getKeyFromString(e.getKey()), e -> SpentInfo.fromProto(e.getValue())))); List confiscatedLockupTxList = new ArrayList<>(proto.getConfiscatedLockupTxListList()); - Map issuanceMap = proto.getIssuanceMapMap().entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> Issuance.fromProto(e.getValue()))); + TreeMap issuanceMap = new TreeMap<>(proto.getIssuanceMapMap().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> Issuance.fromProto(e.getValue())))); List paramChangeList = proto.getParamChangeListList().stream() .map(ParamChange::fromProto).collect(Collectors.toCollection(ArrayList::new)); List evaluatedProposalList = proto.getEvaluatedProposalListList().stream() @@ -218,4 +221,21 @@ public static DaoState fromProto(PB.BsqState proto) { public void setChainHeight(int chainHeight) { this.chainHeight = chainHeight; } + + @Override + public String toString() { + return "DaoState{" + + "\n chainHeight=" + chainHeight + + ",\n blocks=" + blocks + + ",\n cycles=" + cycles + + ",\n unspentTxOutputMap=" + unspentTxOutputMap + + ",\n nonBsqTxOutputMap=" + nonBsqTxOutputMap + + ",\n spentInfoMap=" + spentInfoMap + + ",\n confiscatedLockupTxList=" + confiscatedLockupTxList + + ",\n issuanceMap=" + issuanceMap + + ",\n paramChangeList=" + paramChangeList + + ",\n evaluatedProposalList=" + evaluatedProposalList + + ",\n decryptedBallotsWithMeritsList=" + decryptedBallotsWithMeritsList + + "\n}"; + } } diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTxOutput.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTxOutput.java index e8b5015ccf9..62f5b20c706 100644 --- a/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTxOutput.java +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/BaseTxOutput.java @@ -45,9 +45,10 @@ public abstract class BaseTxOutput implements ImmutableDaoStateModel { protected final long value; protected final String txId; - // Only set if dumpBlockchainData is true + // Before v0.9.6 it was only set if dumpBlockchainData was set to true but we changed that with 0.9.6 + // so that is is always set. We still need to support it because of backward compatibility. @Nullable - protected final PubKeyScript pubKeyScript; + protected final PubKeyScript pubKeyScript; // Has about 50 bytes, total size of TxOutput is about 300 bytes. @Nullable protected final String address; @Nullable @@ -69,7 +70,6 @@ public BaseTxOutput(int index, this.address = address; this.opReturnData = opReturnData; this.blockHeight = blockHeight; - } diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputKey.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputKey.java index 11ebec2a896..282d69d2783 100644 --- a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputKey.java +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputKey.java @@ -21,6 +21,8 @@ import lombok.Value; +import org.jetbrains.annotations.NotNull; + import javax.annotation.concurrent.Immutable; /** @@ -29,7 +31,7 @@ */ @Immutable @Value -public final class TxOutputKey implements ImmutableDaoStateModel { +public final class TxOutputKey implements ImmutableDaoStateModel, Comparable { private final String txId; private final int index; @@ -47,4 +49,9 @@ public static TxOutputKey getKeyFromString(String keyAsString) { final String[] tokens = keyAsString.split(":"); return new TxOutputKey(tokens[0], Integer.valueOf(tokens[1])); } + + @Override + public int compareTo(@NotNull Object o) { + return toString().compareTo(o.toString()); + } } diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index 9a119c57c14..85a02942bc3 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -28,6 +28,15 @@ import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage; import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest; import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesRequest; +import bisq.core.dao.monitoring.network.messages.GetProposalStateHashesResponse; +import bisq.core.dao.monitoring.network.messages.NewBlindVoteStateHashMessage; +import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage; +import bisq.core.dao.monitoring.network.messages.NewProposalStateHashMessage; import bisq.core.dao.node.messages.GetBlocksRequest; import bisq.core.dao.node.messages.GetBlocksResponse; import bisq.core.dao.node.messages.NewBlockBroadcastMessage; @@ -153,7 +162,6 @@ public NetworkEnvelope fromProto(PB.NetworkEnvelope proto) throws ProtobufferExc return GetBlocksResponse.fromProto(proto.getGetBlocksResponse(), messageVersion); case NEW_BLOCK_BROADCAST_MESSAGE: return NewBlockBroadcastMessage.fromProto(proto.getNewBlockBroadcastMessage(), messageVersion); - case ADD_PERSISTABLE_NETWORK_PAYLOAD_MESSAGE: return AddPersistableNetworkPayloadMessage.fromProto(proto.getAddPersistableNetworkPayloadMessage(), this, messageVersion); case ACK_MESSAGE: @@ -161,6 +169,27 @@ public NetworkEnvelope fromProto(PB.NetworkEnvelope proto) throws ProtobufferExc case REPUBLISH_GOVERNANCE_DATA_REQUEST: return RepublishGovernanceDataRequest.fromProto(proto.getRepublishGovernanceDataRequest(), messageVersion); + case NEW_DAO_STATE_HASH_MESSAGE: + return NewDaoStateHashMessage.fromProto(proto.getNewDaoStateHashMessage(), messageVersion); + case GET_DAO_STATE_HASHES_REQUEST: + return GetDaoStateHashesRequest.fromProto(proto.getGetDaoStateHashesRequest(), messageVersion); + case GET_DAO_STATE_HASHES_RESPONSE: + return GetDaoStateHashesResponse.fromProto(proto.getGetDaoStateHashesResponse(), messageVersion); + + case NEW_PROPOSAL_STATE_HASH_MESSAGE: + return NewProposalStateHashMessage.fromProto(proto.getNewProposalStateHashMessage(), messageVersion); + case GET_PROPOSAL_STATE_HASHES_REQUEST: + return GetProposalStateHashesRequest.fromProto(proto.getGetProposalStateHashesRequest(), messageVersion); + case GET_PROPOSAL_STATE_HASHES_RESPONSE: + return GetProposalStateHashesResponse.fromProto(proto.getGetProposalStateHashesResponse(), messageVersion); + + case NEW_BLIND_VOTE_STATE_HASH_MESSAGE: + return NewBlindVoteStateHashMessage.fromProto(proto.getNewBlindVoteStateHashMessage(), messageVersion); + case GET_BLIND_VOTE_STATE_HASHES_REQUEST: + return GetBlindVoteStateHashesRequest.fromProto(proto.getGetBlindVoteStateHashesRequest(), messageVersion); + case GET_BLIND_VOTE_STATE_HASHES_RESPONSE: + return GetBlindVoteStateHashesResponse.fromProto(proto.getGetBlindVoteStateHashesResponse(), messageVersion); + default: throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString()); diff --git a/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java b/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java index 1bf5ce2c90f..a36b129f3c8 100644 --- a/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java +++ b/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java @@ -39,6 +39,7 @@ public static void setSupportedCapabilities(BisqEnvironment bisqEnvironment) { supportedCapabilities.add(Capability.PROPOSAL); supportedCapabilities.add(Capability.BLIND_VOTE); supportedCapabilities.add(Capability.BSQ_BLOCK); + supportedCapabilities.add(Capability.DAO_STATE); String isFullDaoNode = bisqEnvironment.getProperty(DaoOptionKeys.FULL_DAO_NODE, String.class, ""); if (isFullDaoNode != null && !isFullDaoNode.isEmpty()) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index a69439dc803..c9afe3f29b9 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1260,6 +1260,7 @@ dao.tab.bsqWallet=BSQ wallet dao.tab.proposals=Governance dao.tab.bonding=Bonding dao.tab.proofOfBurn=Asset listing fee/Proof of burn +dao.tab.monitor=Network monitor dao.tab.news=News dao.paidWithBsq=paid with BSQ @@ -1311,6 +1312,7 @@ dao.results.proposals.table.header.result=Vote result dao.results.proposals.voting.detail.header=Vote results for selected proposal dao.results.exceptions=Vote result exception(s) + # suppress inspection "UnusedProperty" dao.param.UNDEFINED=Undefined @@ -1742,7 +1744,6 @@ dao.wallet.dashboard.burntTx=No. of all fee payments transactions dao.wallet.dashboard.price=Latest BSQ/BTC trade price (in Bisq) dao.wallet.dashboard.marketCap=Market capitalisation (based on trade price) - dao.wallet.receive.fundYourWallet=Your BSQ receive address dao.wallet.receive.bsqAddress=BSQ wallet address (Fresh unused address) @@ -1857,6 +1858,51 @@ dao.news.DAOOnTestnet.fourthSection.title=4. Explore a BSQ Block Explorer dao.news.DAOOnTestnet.fourthSection.content=Since BSQ is just bitcoin, you can see BSQ transactions on our bitcoin block explorer. dao.news.DAOOnTestnet.readMoreLink=Read the full documentation +dao.monitor.daoState=DAO state +dao.monitor.proposals=Proposals state +dao.monitor.blindVotes=Blind votes state + +dao.monitor.table.peers=Peers +dao.monitor.table.conflicts=Conflicts +dao.monitor.state=Status +dao.monitor.requestAlHashes=Request all hashes +dao.monitor.resync=Resync DAO state +dao.monitor.table.header.cycleBlockHeight=Cycle / block height +dao.monitor.table.cycleBlockHeight=Cycle {0} / block {1} + +dao.monitor.daoState.headline=DAO state +dao.monitor.daoState.daoStateInSync=Your local DAO state is in consensus with the network +dao.monitor.daoState.daoStateNotInSync=Your local DAO state is not in consensus with the network. Please resync your \ + DAO state. +dao.monitor.daoState.table.headline=Chain of DAO state hashes +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.proposal.headline=Proposals state +dao.monitor.proposal.daoStateInSync=Your local proposals state is in consensus with the network +dao.monitor.proposal.daoStateNotInSync=Your local proposals state is not in consensus with the network. Please restart your \ + application. +dao.monitor.proposal.table.headline=Chain of proposal state hashes +dao.monitor.proposal.conflictTable.headline=Proposal state hashes from peers in conflict + +dao.monitor.proposal.table.hash=Hash of proposal state +dao.monitor.proposal.table.prev=Previous hash +dao.monitor.proposal.table.numProposals=No. proposals + + +dao.monitor.blindVote.headline=Blind votes state +dao.monitor.blindVote.daoStateInSync=Your local blind votes state is in consensus with the network +dao.monitor.blindVote.daoStateNotInSync=Your local blind votes state is not in consensus with the network. Please restart your \ + application. +dao.monitor.blindVote.table.headline=Chain of blind vote state hashes +dao.monitor.blindVote.conflictTable.headline=Blind vote state hashes from peers in conflict +dao.monitor.blindVote.table.hash=Hash of blind vote state +dao.monitor.blindVote.table.prev=Previous hash +dao.monitor.blindVote.table.numBlindVotes=No. blind votes + + #################################################################### # Windows #################################################################### diff --git a/core/src/test/java/bisq/core/dao/state/DaoStateServiceTest.java b/core/src/test/java/bisq/core/dao/state/DaoStateServiceTest.java index 0d9eff411b8..3809770dc95 100644 --- a/core/src/test/java/bisq/core/dao/state/DaoStateServiceTest.java +++ b/core/src/test/java/bisq/core/dao/state/DaoStateServiceTest.java @@ -38,6 +38,7 @@ public void testIsBlockHashKnown() { ); Block block = new Block(0, 1534800000, "fakeblockhash0", null); + stateService.onNewBlockHeight(0); stateService.onNewBlockWithEmptyTxs(block); Assert.assertEquals( "Block has to be genesis block to get added.", @@ -52,10 +53,13 @@ public void testIsBlockHashKnown() { ); block = new Block(1, 1534800001, "fakeblockhash1", null); + stateService.onNewBlockHeight(1); stateService.onNewBlockWithEmptyTxs(block); block = new Block(2, 1534800002, "fakeblockhash2", null); + stateService.onNewBlockHeight(2); stateService.onNewBlockWithEmptyTxs(block); block = new Block(3, 1534800003, "fakeblockhash3", null); + stateService.onNewBlockHeight(3); stateService.onNewBlockWithEmptyTxs(block); Assert.assertEquals( "Block that was never added should still not exist after adding more blocks.", diff --git a/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java b/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java index 7941e7bb5a2..d2730b7c303 100644 --- a/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java +++ b/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java @@ -18,6 +18,7 @@ package bisq.core.dao.state; import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; @@ -33,7 +34,7 @@ import static org.powermock.api.mockito.PowerMockito.mock; @RunWith(PowerMockRunner.class) -@PrepareForTest({DaoStateService.class, GenesisTxInfo.class, CycleService.class, DaoStateStorageService.class}) +@PrepareForTest({DaoStateService.class, GenesisTxInfo.class, CycleService.class, DaoStateStorageService.class, DaoStateMonitoringService.class}) @PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*"}) public class DaoStateSnapshotServiceTest { @@ -44,7 +45,8 @@ public void setup() { daoStateSnapshotService = new DaoStateSnapshotService(mock(DaoStateService.class), mock(GenesisTxInfo.class), mock(CycleService.class), - mock(DaoStateStorageService.class)); + mock(DaoStateStorageService.class), + mock(DaoStateMonitoringService.class)); } @Test diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index b2ff670a49d..8a2d2bd26d4 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -2049,6 +2049,14 @@ textfield */ -fx-fill: -fx-accent; } +.dao-inSync { + -fx-text-fill: -bs-rd-green; +} + +.dao-inConflict { + -fx-text-fill: -bs-rd-error-red; +} + /******************************************************************************************************************** * * * Notifications * diff --git a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java index 412645fb567..98239372f88 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java @@ -28,6 +28,7 @@ import bisq.desktop.main.dao.bonding.BondingView; import bisq.desktop.main.dao.burnbsq.BurnBsqView; import bisq.desktop.main.dao.governance.GovernanceView; +import bisq.desktop.main.dao.monitor.MonitorView; import bisq.desktop.main.dao.news.NewsView; import bisq.desktop.main.dao.wallet.BsqWalletView; import bisq.desktop.main.dao.wallet.dashboard.BsqDashboardView; @@ -52,7 +53,7 @@ public class DaoView extends ActivatableViewAndModel { @FXML - private Tab bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, daoNewsTab; + private Tab bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, daoNewsTab, monitor; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; @@ -80,23 +81,26 @@ public void initialize() { proposalsTab = new Tab(Res.get("dao.tab.proposals").toUpperCase()); bondingTab = new Tab(Res.get("dao.tab.bonding").toUpperCase()); burnBsqTab = new Tab(Res.get("dao.tab.proofOfBurn").toUpperCase()); + monitor = new Tab(Res.get("dao.tab.monitor").toUpperCase()); bsqWalletTab.setClosable(false); proposalsTab.setClosable(false); bondingTab.setClosable(false); burnBsqTab.setClosable(false); + monitor.setClosable(false); if (!DevEnv.isDaoActivated()) { + bsqWalletTab.setDisable(true); proposalsTab.setDisable(true); bondingTab.setDisable(true); burnBsqTab.setDisable(true); - bsqWalletTab.setDisable(true); + monitor.setDisable(true); daoNewsTab = new Tab(Res.get("dao.tab.news").toUpperCase()); root.getTabs().add(daoNewsTab); } else { - root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab, burnBsqTab); + root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab, burnBsqTab, monitor); } navigationListener = viewPath -> { @@ -121,6 +125,8 @@ public void initialize() { navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); } else if (newValue == burnBsqTab) { navigation.navigateTo(MainView.class, DaoView.class, BurnBsqView.class); + } else if (newValue == monitor) { + navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class); } }; } @@ -141,6 +147,8 @@ else if (selectedItem == bondingTab) navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); else if (selectedItem == burnBsqTab) navigation.navigateTo(MainView.class, DaoView.class, BurnBsqView.class); + else if (selectedItem == monitor) + navigation.navigateTo(MainView.class, DaoView.class, MonitorView.class); } } else { loadView(NewsView.class); @@ -173,6 +181,8 @@ private void loadView(Class viewClass) { selectedTab = bondingTab; } else if (view instanceof BurnBsqView) { selectedTab = burnBsqTab; + } else if (view instanceof MonitorView) { + selectedTab = monitor; } else if (view instanceof NewsView) { selectedTab = daoNewsTab; } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.java index e263a6a2b34..14a42e934a0 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/bonds/BondsView.java @@ -107,6 +107,7 @@ protected void activate() { bondedReputationRepository.getBonds().addListener(bondedReputationListener); bondedRolesRepository.getBonds().addListener(bondedRolesListener); updateList(); + GUIUtil.setFitToRowsForTableView(tableView, 37, 28, 2, 30); } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.java index e189b069e00..0f9225e9327 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/reputation/MyReputationView.java @@ -189,6 +189,7 @@ protected void activate() { setNewRandomSalt(); updateList(); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 30); } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.java index 264979523b7..2340ea7f936 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/RolesView.java @@ -103,6 +103,7 @@ protected void activate() { sortedList.comparatorProperty().bind(tableView.comparatorProperty()); daoFacade.getBondedRoles().addListener(bondedRoleListChangeListener); updateList(); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 30); } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java index 1760b32f4b5..d6fad9d71a4 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java @@ -191,6 +191,7 @@ protected void activate() { }); updateList(); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 10); updateButtonState(); feeAmountInputTextField.resetValidation(); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java index af5b9ab7adb..b4a51fa1681 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java @@ -187,6 +187,8 @@ protected void activate() { preImageTextField.setValidator(new InputValidator()); updateList(); + GUIUtil.setFitToRowsForTableView(myItemsTableView, 41, 28, 2, 4); + GUIUtil.setFitToRowsForTableView(allTxsTableView, 41, 28, 2, 10); updateButtonState(); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java index dd275f1ddc4..3c3c4510c47 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java @@ -51,7 +51,6 @@ @FxmlView public class GovernanceView extends ActivatableViewAndModel { - private final ViewLoader viewLoader; private final Navigation navigation; private final DaoFacade daoFacade; @@ -102,6 +101,7 @@ public void initialize() { ProposalsView.class, baseNavPath); result = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.result"), VoteResultView.class, baseNavPath); + leftVBox.getChildren().addAll(dashboard, make, open, result); } 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 3e8d3615cd0..2a7ad6b7842 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 @@ -184,8 +184,6 @@ private ProposalsView(DaoFacade daoFacade, public void initialize() { super.initialize(); - root.getStyleClass().add("vote-root"); - gridRow = phasesView.addGroup(root, gridRow); proposalDisplayGridPane = new GridPane(); @@ -226,6 +224,7 @@ protected void activate() { bsqWalletService.getUnlockingBondsBalance()); updateListItems(); + GUIUtil.setFitToRowsForTableView(tableView, 38, 28, 2, 6); updateViews(); } @@ -334,8 +333,6 @@ private void updateListItems() { } GUIUtil.setFitToRowsForTableView(tableView, 38, 28, 2, 6); - tableView.layout(); - root.layout(); } private void createAllFieldsOnProposalDisplay(Proposal proposal, @Nullable Ballot ballot, 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 633ec79181a..ccb850a7e35 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 @@ -149,6 +149,7 @@ public class VoteResultView extends ActivatableView implements D private ChangeListener selectedVoteResultListItemListener; private ResultsOfCycle resultsOfCycle; private ProposalListItem selectedProposalListItem; + private TableView votesTableView; /////////////////////////////////////////////////////////////////////////////////////////// @@ -211,6 +212,13 @@ protected void activate() { JsonElement cyclesJsonArray = getVotingHistoryJson(); GUIUtil.exportJSON("voteResultsHistory.json", cyclesJsonArray, (Stage) root.getScene().getWindow()); }); + if (proposalsTableView != null) { + GUIUtil.setFitToRowsForTableView(proposalsTableView, 25, 28, 2, 4); + } + if (votesTableView != null) { + GUIUtil.setFitToRowsForTableView(votesTableView, 25, 28, 2, 4); + } + GUIUtil.setFitToRowsForTableView(cyclesTableView, 25, 28, 2, 4); } @Override @@ -513,7 +521,7 @@ private void createVotesTable() { GridPane.setColumnSpan(votesTableHeader, 2); root.getChildren().add(votesTableHeader); - TableView votesTableView = new TableView<>(); + votesTableView = new TableView<>(); votesTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); votesTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/MonitorView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/monitor/MonitorView.fxml new file mode 100644 index 00000000000..c0377d39c31 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/MonitorView.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/MonitorView.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/MonitorView.java new file mode 100644 index 00000000000..93de1642514 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/MonitorView.java @@ -0,0 +1,131 @@ +/* + * 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.desktop.main.dao.monitor; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.components.MenuItem; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.monitor.blindvotes.BlindVoteStateMonitorView; +import bisq.desktop.main.dao.monitor.daostate.DaoStateMonitorView; +import bisq.desktop.main.dao.monitor.proposals.ProposalStateMonitorView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import java.util.Arrays; +import java.util.List; + +@FxmlView +public class MonitorView extends ActivatableViewAndModel { + private final ViewLoader viewLoader; + private final Navigation navigation; + + private MenuItem daoState, proposals, blindVotes; + private Navigation.Listener navigationListener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + private Class selectedViewClass; + private ToggleGroup toggleGroup; + + @Inject + private MonitorView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; + } + + @Override + public void initialize() { + navigationListener = viewPath -> { + if (viewPath.size() != 4 || viewPath.indexOf(MonitorView.class) != 2) + return; + + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + toggleGroup = new ToggleGroup(); + List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, MonitorView.class); + daoState = new MenuItem(navigation, toggleGroup, Res.get("dao.monitor.daoState"), + DaoStateMonitorView.class, baseNavPath); + proposals = new MenuItem(navigation, toggleGroup, Res.get("dao.monitor.proposals"), + ProposalStateMonitorView.class, baseNavPath); + blindVotes = new MenuItem(navigation, toggleGroup, Res.get("dao.monitor.blindVotes"), + BlindVoteStateMonitorView.class, baseNavPath); + + leftVBox.getChildren().addAll(daoState, proposals, blindVotes); + } + + @Override + protected void activate() { + proposals.activate(); + blindVotes.activate(); + daoState.activate(); + + navigation.addListener(navigationListener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(MonitorView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = DaoStateMonitorView.class; + + loadView(selectedViewClass); + + } else if (viewPath.size() == 4 && viewPath.indexOf(MonitorView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } + } + + @SuppressWarnings("Duplicates") + @Override + protected void deactivate() { + navigation.removeListener(navigationListener); + + proposals.deactivate(); + blindVotes.deactivate(); + daoState.deactivate(); + } + + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); + + if (view instanceof DaoStateMonitorView) toggleGroup.selectToggle(daoState); + else if (view instanceof ProposalStateMonitorView) toggleGroup.selectToggle(proposals); + else if (view instanceof BlindVoteStateMonitorView) toggleGroup.selectToggle(blindVotes); + } +} + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateBlockListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateBlockListItem.java new file mode 100644 index 00000000000..7062fa726e7 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateBlockListItem.java @@ -0,0 +1,52 @@ +/* + * 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.desktop.main.dao.monitor; + +import bisq.core.dao.monitoring.model.StateBlock; +import bisq.core.dao.monitoring.model.StateHash; +import bisq.core.locale.Res; + +import bisq.common.util.Utilities; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@EqualsAndHashCode +public abstract class StateBlockListItem> { + protected final StateBlock stateBlock; + protected final String height; + protected final String hash; + protected final String prevHash; + protected final String numNetworkMessages; + protected final String numMisMatches; + protected final boolean isInSync; + + protected StateBlockListItem(StB stateBlock, int cycleIndex) { + this.stateBlock = stateBlock; + height = Res.get("dao.monitor.table.cycleBlockHeight", cycleIndex + 1, String.valueOf(stateBlock.getHeight())); + hash = Utilities.bytesAsHexString(stateBlock.getHash()); + prevHash = stateBlock.getPrevHash().length > 0 ? Utilities.bytesAsHexString(stateBlock.getPrevHash()) : "-"; + numNetworkMessages = String.valueOf(stateBlock.getPeersMap().size()); + int size = stateBlock.getInConflictMap().size(); + numMisMatches = String.valueOf(size); + isInSync = size == 0; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateInConflictListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateInConflictListItem.java new file mode 100644 index 00000000000..0610f3c8cb9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateInConflictListItem.java @@ -0,0 +1,47 @@ +/* + * 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.desktop.main.dao.monitor; + +import bisq.core.dao.monitoring.model.StateHash; +import bisq.core.locale.Res; + +import bisq.common.util.Utilities; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@EqualsAndHashCode +public abstract class StateInConflictListItem { + private final String peerAddress; + private final String height; + private final String hash; + private final String prevHash; + private final T stateHash; + + protected StateInConflictListItem(String peerAddress, T stateHash, int cycleIndex) { + this.stateHash = stateHash; + this.peerAddress = peerAddress; + height = Res.get("dao.monitor.table.cycleBlockHeight", cycleIndex + 1, String.valueOf(stateHash.getHeight())); + hash = Utilities.bytesAsHexString(stateHash.getHash()); + prevHash = stateHash.getPrevHash().length > 0 ? + Utilities.bytesAsHexString(stateHash.getPrevHash()) : "-"; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateMonitorView.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateMonitorView.java new file mode 100644 index 00000000000..a00059bf9c1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateMonitorView.java @@ -0,0 +1,563 @@ +/* + * 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.desktop.main.dao.monitor; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.TableGroupHeadline; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.monitoring.model.StateBlock; +import bisq.core.dao.monitoring.model.StateHash; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +import javafx.geometry.Insets; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleBooleanProperty; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; + +@FxmlView +public abstract class StateMonitorView, + BLI extends StateBlockListItem, + CLI extends StateInConflictListItem> + extends ActivatableView implements DaoStateListener { + protected final DaoStateService daoStateService; + protected final DaoFacade daoFacade; + protected final CycleService cycleService; + protected final PeriodService periodService; + + protected TextField statusTextField; + protected Button resyncButton; + protected TableView tableView; + protected TableView conflictTableView; + + protected final ObservableList listItems = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(listItems); + private final ObservableList conflictListItems = FXCollections.observableArrayList(); + private final SortedList sortedConflictList = new SortedList<>(conflictListItems); + + protected int gridRow = 0; + private Subscription selectedItemSubscription; + protected final BooleanProperty isInConflict = new SimpleBooleanProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public StateMonitorView(DaoStateService daoStateService, + DaoFacade daoFacade, + CycleService cycleService, + PeriodService periodService) { + this.daoStateService = daoStateService; + this.daoFacade = daoFacade; + this.cycleService = cycleService; + this.periodService = periodService; + } + + @Override + public void initialize() { + createTableView(); + createDetailsView(); + } + + @Override + protected void activate() { + selectedItemSubscription = EasyBind.subscribe(tableView.getSelectionModel().selectedItemProperty(), this::onSelectItem); + + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + sortedConflictList.comparatorProperty().bind(conflictTableView.comparatorProperty()); + + daoStateService.addDaoStateListener(this); + + resyncButton.visibleProperty().bind(isInConflict); + resyncButton.managedProperty().bind(isInConflict); + + if (daoStateService.isParseBlockChainComplete()) { + onDataUpdate(); + } + + GUIUtil.setFitToRowsForTableView(tableView, 25, 28, 2, 5); + GUIUtil.setFitToRowsForTableView(conflictTableView, 38, 28, 2, 4); + } + + @Override + protected void deactivate() { + selectedItemSubscription.unsubscribe(); + + sortedList.comparatorProperty().unbind(); + sortedConflictList.comparatorProperty().unbind(); + + daoStateService.removeDaoStateListener(this); + + resyncButton.visibleProperty().unbind(); + resyncButton.managedProperty().unbind(); + + resyncButton.setOnAction(null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract BLI getStateBlockListItem(StB e); + + protected abstract CLI getStateInConflictListItem(Map.Entry mapEntry); + + protected abstract void requestHashesFromGenesisBlockHeight(String peerAddress); + + protected abstract String getConflictsTableHeader(); + + protected abstract String getPeersTableHeader(); + + protected abstract String getPrevHashTableHeader(); + + protected abstract String getHashTableHeader(); + + protected abstract String getBlockHeightTableHeader(); + + protected abstract String getRequestHashes(); + + protected abstract String getTableHeadLine(); + + protected abstract String getConflictTableHeadLine(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onParseBlockChainComplete() { + onDataUpdate(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Create table views + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createTableView() { + TableGroupHeadline headline = new TableGroupHeadline(getTableHeadLine()); + GridPane.setRowIndex(headline, ++gridRow); + GridPane.setMargin(headline, new Insets(Layout.GROUP_DISTANCE, -10, -10, -10)); + root.getChildren().add(headline); + + tableView = new TableView<>(); + tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + tableView.setPrefHeight(100); + + createColumns(); + GridPane.setRowIndex(tableView, gridRow); + GridPane.setHgrow(tableView, Priority.ALWAYS); + GridPane.setMargin(tableView, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, -10, -25, -10)); + root.getChildren().add(tableView); + + tableView.setItems(sortedList); + } + + private void createDetailsView() { + TableGroupHeadline conflictTableHeadline = new TableGroupHeadline(getConflictTableHeadLine()); + GridPane.setRowIndex(conflictTableHeadline, ++gridRow); + GridPane.setMargin(conflictTableHeadline, new Insets(Layout.GROUP_DISTANCE, -10, -10, -10)); + root.getChildren().add(conflictTableHeadline); + + conflictTableView = new TableView<>(); + conflictTableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); + conflictTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + conflictTableView.setPrefHeight(100); + + createConflictColumns(); + GridPane.setRowIndex(conflictTableView, gridRow); + GridPane.setHgrow(conflictTableView, Priority.ALWAYS); + GridPane.setMargin(conflictTableView, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, -10, -25, -10)); + root.getChildren().add(conflictTableView); + + conflictTableView.setItems(sortedConflictList); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Handler + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onSelectItem(BLI item) { + if (item != null) { + conflictListItems.setAll(item.getStateBlock().getInConflictMap().entrySet().stream() + .map(this::getStateInConflictListItem).collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(conflictTableView, 38, 28, 2, 4); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void onDataUpdate() { + GUIUtil.setFitToRowsForTableView(tableView, 25, 28, 2, 5); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TableColumns + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void createColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(getBlockHeightTableHeader()); + column.setMinWidth(120); + column.getStyleClass().add("first-column"); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getHeight()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getStateBlock().getHeight())); + column.setSortType(TableColumn.SortType.DESCENDING); + tableView.getSortOrder().add(column); + tableView.getColumns().add(column); + + + column = new AutoTooltipTableColumn<>(getHashTableHeader()); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getHash()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(BLI::getHash)); + tableView.getColumns().add(column); + + + column = new AutoTooltipTableColumn<>(getPrevHashTableHeader()); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getPrevHash()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(BLI::getPrevHash)); + tableView.getColumns().add(column); + + + column = new AutoTooltipTableColumn<>(getPeersTableHeader()); + column.setMinWidth(80); + column.setMaxWidth(column.getMinWidth()); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getNumNetworkMessages()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getStateBlock().getPeersMap().size())); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(getConflictsTableHeader()); + column.setMinWidth(80); + column.setMaxWidth(column.getMinWidth()); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getNumMisMatches()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getStateBlock().getInConflictMap().size())); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(""); + column.setMinWidth(40); + column.setMaxWidth(column.getMinWidth()); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final BLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + Label icon; + if (!item.getStateBlock().getPeersMap().isEmpty()) { + if (item.isInSync()) { + icon = FormBuilder.getIcon(AwesomeIcon.OK_CIRCLE); + icon.getStyleClass().addAll("icon", "dao-inSync"); + } else { + icon = FormBuilder.getIcon(AwesomeIcon.REMOVE_CIRCLE); + icon.getStyleClass().addAll("icon", "dao-inConflict"); + } + setGraphic(icon); + } else { + setGraphic(null); + } + } else { + setGraphic(null); + } + } + }; + } + }); + column.setSortable(false); + tableView.getColumns().add(column); + } + + + protected void createConflictColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(getBlockHeightTableHeader()); + column.setMinWidth(120); + column.getStyleClass().add("first-column"); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final CLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getHeight()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getStateHash().getHeight())); + column.setSortType(TableColumn.SortType.DESCENDING); + conflictTableView.getColumns().add(column); + conflictTableView.getSortOrder().add(column); + + + column = new AutoTooltipTableColumn<>(getPeersTableHeader()); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final CLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getPeerAddress()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(CLI::getPeerAddress)); + conflictTableView.getColumns().add(column); + + + column = new AutoTooltipTableColumn<>(getHashTableHeader()); + column.setMinWidth(150); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final CLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getHash()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(CLI::getHash)); + conflictTableView.getColumns().add(column); + + + column = new AutoTooltipTableColumn<>(getPrevHashTableHeader()); + column.setMinWidth(150); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final CLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getPrevHash()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(CLI::getPrevHash)); + conflictTableView.getColumns().add(column); + + + column = new AutoTooltipTableColumn<>(""); + column.setMinWidth(100); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final CLI item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + if (button == null) { + button = new AutoTooltipButton(getRequestHashes()); + setGraphic(button); + } + button.setOnAction(e -> requestHashesFromGenesisBlockHeight(item.getPeerAddress())); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + column.setSortable(false); + conflictTableView.getColumns().add(column); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateBlockListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateBlockListItem.java new file mode 100644 index 00000000000..870ab9407c6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateBlockListItem.java @@ -0,0 +1,40 @@ +/* + * 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.desktop.main.dao.monitor.blindvotes; + +import bisq.desktop.main.dao.monitor.StateBlockListItem; + +import bisq.core.dao.monitoring.model.BlindVoteStateBlock; +import bisq.core.dao.monitoring.model.BlindVoteStateHash; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +@EqualsAndHashCode(callSuper = true) +class BlindVoteStateBlockListItem extends StateBlockListItem { + private final String numBlindVotes; + + BlindVoteStateBlockListItem(BlindVoteStateBlock stateBlock, int cycleIndex) { + super(stateBlock, cycleIndex); + + numBlindVotes = String.valueOf(stateBlock.getNumBlindVotes()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateInConflictListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateInConflictListItem.java new file mode 100644 index 00000000000..a620675e8f3 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateInConflictListItem.java @@ -0,0 +1,39 @@ +/* + * 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.desktop.main.dao.monitor.blindvotes; + +import bisq.desktop.main.dao.monitor.StateInConflictListItem; + +import bisq.core.dao.monitoring.model.BlindVoteStateHash; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +@EqualsAndHashCode(callSuper = true) +class BlindVoteStateInConflictListItem extends StateInConflictListItem { + private final String numBlindVotes; + + BlindVoteStateInConflictListItem(String peerAddress, BlindVoteStateHash stateHash, int cycleIndex) { + super(peerAddress, stateHash, cycleIndex); + + numBlindVotes = String.valueOf(stateHash.getNumBlindVotes()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateMonitorView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateMonitorView.fxml new file mode 100644 index 00000000000..64a9a02ff9a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateMonitorView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateMonitorView.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateMonitorView.java new file mode 100644 index 00000000000..48b37d4b114 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/blindvotes/BlindVoteStateMonitorView.java @@ -0,0 +1,257 @@ +/* + * 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.desktop.main.dao.monitor.blindvotes; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.main.dao.monitor.StateMonitorView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.monitoring.BlindVoteStateMonitoringService; +import bisq.core.dao.monitoring.model.BlindVoteStateBlock; +import bisq.core.dao.monitoring.model.BlindVoteStateHash; +import bisq.core.dao.state.DaoStateService; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import javafx.util.Callback; + +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; + +@FxmlView +public class BlindVoteStateMonitorView extends StateMonitorView + implements BlindVoteStateMonitoringService.Listener { + private final BlindVoteStateMonitoringService blindVoteStateMonitoringService; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Inject + private BlindVoteStateMonitorView(DaoStateService daoStateService, + DaoFacade daoFacade, + BlindVoteStateMonitoringService blindVoteStateMonitoringService, + CycleService cycleService, + PeriodService periodService) { + super(daoStateService, daoFacade, cycleService, periodService); + + this.blindVoteStateMonitoringService = blindVoteStateMonitoringService; + } + + @Override + public void initialize() { + FormBuilder.addTitledGroupBg(root, gridRow, 3, Res.get("dao.monitor.blindVote.headline")); + + statusTextField = FormBuilder.addTopLabelTextField(root, ++gridRow, + Res.get("dao.monitor.state")).second; + resyncButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.monitor.resync"), 10); + + super.initialize(); + } + + @Override + protected void activate() { + super.activate(); + blindVoteStateMonitoringService.addListener(this); + + resyncButton.setOnAction(e -> daoFacade.resyncDao(() -> + new Popup<>().attention(Res.get("setting.preferences.dao.resync.popup")) + .useShutDownButton() + .hideCloseButton() + .show()) + ); + } + + @Override + protected void deactivate() { + super.deactivate(); + blindVoteStateMonitoringService.removeListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BlindVoteStateMonitoringService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onBlindVoteStateBlockChainChanged() { + if (daoStateService.isParseBlockChainComplete()) { + onDataUpdate(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implementation abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected BlindVoteStateBlockListItem getStateBlockListItem(BlindVoteStateBlock daoStateBlock) { + int cycleIndex = periodService.getCycle(daoStateBlock.getHeight()).map(cycleService::getCycleIndex).orElse(0); + return new BlindVoteStateBlockListItem(daoStateBlock, cycleIndex); + } + + @Override + protected BlindVoteStateInConflictListItem getStateInConflictListItem(Map.Entry mapEntry) { + BlindVoteStateHash blindVoteStateHash = mapEntry.getValue(); + int cycleIndex = periodService.getCycle(blindVoteStateHash.getHeight()).map(cycleService::getCycleIndex).orElse(0); + return new BlindVoteStateInConflictListItem(mapEntry.getKey(), mapEntry.getValue(), cycleIndex); + } + + @Override + protected String getTableHeadLine() { + return Res.get("dao.monitor.blindVote.table.headline"); + } + + @Override + protected String getConflictTableHeadLine() { + return Res.get("dao.monitor.blindVote.conflictTable.headline"); + } + + @Override + protected String getConflictsTableHeader() { + return Res.get("dao.monitor.table.conflicts"); + } + + @Override + protected String getPeersTableHeader() { + return Res.get("dao.monitor.table.peers"); + } + + @Override + protected String getPrevHashTableHeader() { + return Res.get("dao.monitor.blindVote.table.prev"); + } + + @Override + protected String getHashTableHeader() { + return Res.get("dao.monitor.blindVote.table.hash"); + } + + @Override + protected String getBlockHeightTableHeader() { + return Res.get("dao.monitor.table.header.cycleBlockHeight"); + } + + @Override + protected String getRequestHashes() { + return Res.get("dao.monitor.requestAlHashes"); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Override + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onDataUpdate() { + isInConflict.set(blindVoteStateMonitoringService.isInConflict()); + + if (isInConflict.get()) { + statusTextField.setText(Res.get("dao.monitor.blindVote.daoStateNotInSync")); + statusTextField.getStyleClass().add("dao-inConflict"); + } else { + statusTextField.setText(Res.get("dao.monitor.blindVote.daoStateInSync")); + statusTextField.getStyleClass().remove("dao-inConflict"); + } + + listItems.setAll(blindVoteStateMonitoringService.getBlindVoteStateBlockChain().stream() + .map(this::getStateBlockListItem) + .collect(Collectors.toList())); + + super.onDataUpdate(); + } + + @Override + protected void requestHashesFromGenesisBlockHeight(String peerAddress) { + blindVoteStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress); + } + + @Override + protected void createColumns() { + super.createColumns(); + + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.blindVote.table.numBlindVotes")); + column.setMinWidth(110); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(BlindVoteStateBlockListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getNumBlindVotes()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getStateBlock().getMyStateHash().getNumBlindVotes())); + tableView.getColumns().add(1, column); + } + + + protected void createConflictColumns() { + super.createConflictColumns(); + + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.blindVote.table.numBlindVotes")); + column.setMinWidth(110); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(BlindVoteStateInConflictListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getNumBlindVotes()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getStateHash().getNumBlindVotes())); + conflictTableView.getColumns().add(1, column); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateBlockListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateBlockListItem.java new file mode 100644 index 00000000000..d4ec7fdd369 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateBlockListItem.java @@ -0,0 +1,37 @@ +/* + * 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.desktop.main.dao.monitor.daostate; + +import bisq.desktop.main.dao.monitor.StateBlockListItem; + +import bisq.core.dao.monitoring.model.DaoStateBlock; +import bisq.core.dao.monitoring.model.DaoStateHash; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +@EqualsAndHashCode(callSuper = true) +class DaoStateBlockListItem extends StateBlockListItem { + DaoStateBlockListItem(DaoStateBlock stateBlock, int cycleIndex) { + super(stateBlock, cycleIndex); + } +} + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateInConflictListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateInConflictListItem.java new file mode 100644 index 00000000000..1496d97d6b5 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateInConflictListItem.java @@ -0,0 +1,35 @@ +/* + * 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.desktop.main.dao.monitor.daostate; + +import bisq.desktop.main.dao.monitor.StateInConflictListItem; + +import bisq.core.dao.monitoring.model.DaoStateHash; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +@EqualsAndHashCode(callSuper = true) +class DaoStateInConflictListItem extends StateInConflictListItem { + DaoStateInConflictListItem(String peerAddress, DaoStateHash stateHash, int cycleIndex) { + super(peerAddress, stateHash, cycleIndex); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.fxml new file mode 100644 index 00000000000..c68fbf5c5bf --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + 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 new file mode 100644 index 00000000000..e5b9cb79d1e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java @@ -0,0 +1,188 @@ +/* + * 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.desktop.main.dao.monitor.daostate; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.dao.monitor.StateMonitorView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.monitoring.DaoStateMonitoringService; +import bisq.core.dao.monitoring.model.DaoStateBlock; +import bisq.core.dao.monitoring.model.DaoStateHash; +import bisq.core.dao.state.DaoStateService; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import java.util.Map; +import java.util.stream.Collectors; + +@FxmlView +public class DaoStateMonitorView extends StateMonitorView + implements DaoStateMonitoringService.Listener { + private final DaoStateMonitoringService daoStateMonitoringService; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Inject + private DaoStateMonitorView(DaoStateService daoStateService, + DaoFacade daoFacade, + DaoStateMonitoringService daoStateMonitoringService, + CycleService cycleService, + PeriodService periodService) { + super(daoStateService, daoFacade, cycleService, periodService); + + this.daoStateMonitoringService = daoStateMonitoringService; + } + + @Override + public void initialize() { + FormBuilder.addTitledGroupBg(root, gridRow, 3, Res.get("dao.monitor.daoState.headline")); + + statusTextField = FormBuilder.addTopLabelTextField(root, ++gridRow, + Res.get("dao.monitor.state")).second; + resyncButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.monitor.resync"), 10); + + super.initialize(); + } + + @Override + protected void activate() { + super.activate(); + daoStateMonitoringService.addListener(this); + + resyncButton.setOnAction(e -> daoFacade.resyncDao(() -> + new Popup<>().attention(Res.get("setting.preferences.dao.resync.popup")) + .useShutDownButton() + .hideCloseButton() + .show()) + ); + } + + @Override + protected void deactivate() { + super.deactivate(); + daoStateMonitoringService.removeListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateMonitoringService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onChangeAfterBatchProcessing() { + if (daoStateService.isParseBlockChainComplete()) { + onDataUpdate(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implementation abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected DaoStateBlockListItem getStateBlockListItem(DaoStateBlock daoStateBlock) { + int cycleIndex = periodService.getCycle(daoStateBlock.getHeight()).map(cycleService::getCycleIndex).orElse(0); + return new DaoStateBlockListItem(daoStateBlock, cycleIndex); + } + + @Override + protected DaoStateInConflictListItem getStateInConflictListItem(Map.Entry mapEntry) { + DaoStateHash daoStateHash = mapEntry.getValue(); + int cycleIndex = periodService.getCycle(daoStateHash.getHeight()).map(cycleService::getCycleIndex).orElse(0); + return new DaoStateInConflictListItem(mapEntry.getKey(), daoStateHash, cycleIndex); + } + + @Override + protected String getTableHeadLine() { + return Res.get("dao.monitor.daoState.table.headline"); + } + + @Override + protected String getConflictTableHeadLine() { + return Res.get("dao.monitor.daoState.conflictTable.headline"); + } + + @Override + protected String getConflictsTableHeader() { + return Res.get("dao.monitor.table.conflicts"); + } + + @Override + protected String getPeersTableHeader() { + return Res.get("dao.monitor.table.peers"); + } + + @Override + protected String getPrevHashTableHeader() { + return Res.get("dao.monitor.daoState.table.prev"); + } + + @Override + protected String getHashTableHeader() { + return Res.get("dao.monitor.daoState.table.hash"); + } + + @Override + protected String getBlockHeightTableHeader() { + return Res.get("dao.monitor.daoState.table.blockHeight"); + } + + @Override + protected String getRequestHashes() { + return Res.get("dao.monitor.requestAlHashes"); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Override + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onDataUpdate() { + isInConflict.set(daoStateMonitoringService.isInConflict()); + + if (isInConflict.get()) { + statusTextField.setText(Res.get("dao.monitor.daoState.daoStateNotInSync")); + statusTextField.getStyleClass().add("dao-inConflict"); + } else { + statusTextField.setText(Res.get("dao.monitor.daoState.daoStateInSync")); + statusTextField.getStyleClass().remove("dao-inConflict"); + } + + listItems.setAll(daoStateMonitoringService.getDaoStateBlockChain().stream() + .map(this::getStateBlockListItem) + .collect(Collectors.toList())); + + super.onDataUpdate(); + } + + @Override + protected void requestHashesFromGenesisBlockHeight(String peerAddress) { + daoStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateBlockListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateBlockListItem.java new file mode 100644 index 00000000000..abb47480da9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateBlockListItem.java @@ -0,0 +1,40 @@ +/* + * 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.desktop.main.dao.monitor.proposals; + +import bisq.desktop.main.dao.monitor.StateBlockListItem; + +import bisq.core.dao.monitoring.model.ProposalStateBlock; +import bisq.core.dao.monitoring.model.ProposalStateHash; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +@EqualsAndHashCode(callSuper = true) +class ProposalStateBlockListItem extends StateBlockListItem { + private final String numProposals; + + ProposalStateBlockListItem(ProposalStateBlock stateBlock, int cycleIndex) { + super(stateBlock, cycleIndex); + + numProposals = String.valueOf(stateBlock.getNumProposals()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateInConflictListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateInConflictListItem.java new file mode 100644 index 00000000000..af006fa6aab --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateInConflictListItem.java @@ -0,0 +1,39 @@ +/* + * 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.desktop.main.dao.monitor.proposals; + +import bisq.desktop.main.dao.monitor.StateInConflictListItem; + +import bisq.core.dao.monitoring.model.ProposalStateHash; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +@EqualsAndHashCode(callSuper = true) +class ProposalStateInConflictListItem extends StateInConflictListItem { + private final String numProposals; + + ProposalStateInConflictListItem(String peerAddress, ProposalStateHash stateHash, int cycleIndex) { + super(peerAddress, stateHash, cycleIndex); + + numProposals = String.valueOf(stateHash.getNumProposals()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateMonitorView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateMonitorView.fxml new file mode 100644 index 00000000000..37124f77f82 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateMonitorView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateMonitorView.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateMonitorView.java new file mode 100644 index 00000000000..2f0156fe2d8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/proposals/ProposalStateMonitorView.java @@ -0,0 +1,257 @@ +/* + * 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.desktop.main.dao.monitor.proposals; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.main.dao.monitor.StateMonitorView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.monitoring.ProposalStateMonitoringService; +import bisq.core.dao.monitoring.model.ProposalStateBlock; +import bisq.core.dao.monitoring.model.ProposalStateHash; +import bisq.core.dao.state.DaoStateService; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import javafx.util.Callback; + +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; + +@FxmlView +public class ProposalStateMonitorView extends StateMonitorView + implements ProposalStateMonitoringService.Listener { + private final ProposalStateMonitoringService proposalStateMonitoringService; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Inject + private ProposalStateMonitorView(DaoStateService daoStateService, + DaoFacade daoFacade, + ProposalStateMonitoringService proposalStateMonitoringService, + CycleService cycleService, + PeriodService periodService) { + super(daoStateService, daoFacade, cycleService, periodService); + + this.proposalStateMonitoringService = proposalStateMonitoringService; + } + + @Override + public void initialize() { + FormBuilder.addTitledGroupBg(root, gridRow, 3, Res.get("dao.monitor.proposal.headline")); + + statusTextField = FormBuilder.addTopLabelTextField(root, ++gridRow, + Res.get("dao.monitor.state")).second; + resyncButton = FormBuilder.addButton(root, ++gridRow, Res.get("dao.monitor.resync"), 10); + + super.initialize(); + } + + @Override + protected void activate() { + super.activate(); + proposalStateMonitoringService.addListener(this); + + resyncButton.setOnAction(e -> daoFacade.resyncDao(() -> + new Popup<>().attention(Res.get("setting.preferences.dao.resync.popup")) + .useShutDownButton() + .hideCloseButton() + .show()) + ); + } + + @Override + protected void deactivate() { + super.deactivate(); + proposalStateMonitoringService.removeListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // ProposalStateMonitoringService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onProposalStateBlockChainChanged() { + if (daoStateService.isParseBlockChainComplete()) { + onDataUpdate(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implementation abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected ProposalStateBlockListItem getStateBlockListItem(ProposalStateBlock daoStateBlock) { + int cycleIndex = periodService.getCycle(daoStateBlock.getHeight()).map(cycleService::getCycleIndex).orElse(0); + return new ProposalStateBlockListItem(daoStateBlock, cycleIndex); + } + + @Override + protected ProposalStateInConflictListItem getStateInConflictListItem(Map.Entry mapEntry) { + ProposalStateHash proposalStateHash = mapEntry.getValue(); + int cycleIndex = periodService.getCycle(proposalStateHash.getHeight()).map(cycleService::getCycleIndex).orElse(0); + return new ProposalStateInConflictListItem(mapEntry.getKey(), mapEntry.getValue(), cycleIndex); + } + + @Override + protected String getTableHeadLine() { + return Res.get("dao.monitor.proposal.table.headline"); + } + + @Override + protected String getConflictTableHeadLine() { + return Res.get("dao.monitor.proposal.conflictTable.headline"); + } + + @Override + protected String getConflictsTableHeader() { + return Res.get("dao.monitor.table.conflicts"); + } + + @Override + protected String getPeersTableHeader() { + return Res.get("dao.monitor.table.peers"); + } + + @Override + protected String getPrevHashTableHeader() { + return Res.get("dao.monitor.proposal.table.prev"); + } + + @Override + protected String getHashTableHeader() { + return Res.get("dao.monitor.proposal.table.hash"); + } + + @Override + protected String getBlockHeightTableHeader() { + return Res.get("dao.monitor.table.header.cycleBlockHeight"); + } + + @Override + protected String getRequestHashes() { + return Res.get("dao.monitor.requestAlHashes"); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Override + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void onDataUpdate() { + isInConflict.set(proposalStateMonitoringService.isInConflict()); + + if (isInConflict.get()) { + statusTextField.setText(Res.get("dao.monitor.proposal.daoStateNotInSync")); + statusTextField.getStyleClass().add("dao-inConflict"); + } else { + statusTextField.setText(Res.get("dao.monitor.proposal.daoStateInSync")); + statusTextField.getStyleClass().remove("dao-inConflict"); + } + + listItems.setAll(proposalStateMonitoringService.getProposalStateBlockChain().stream() + .map(this::getStateBlockListItem) + .collect(Collectors.toList())); + + super.onDataUpdate(); + } + + @Override + protected void requestHashesFromGenesisBlockHeight(String peerAddress) { + proposalStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress); + } + + @Override + protected void createColumns() { + super.createColumns(); + + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.proposal.table.numProposals")); + column.setMinWidth(110); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(ProposalStateBlockListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getNumProposals()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getStateBlock().getMyStateHash().getNumProposals())); + tableView.getColumns().add(1, column); + } + + + protected void createConflictColumns() { + super.createConflictColumns(); + + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.proposal.table.numProposals")); + column.setMinWidth(110); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(ProposalStateInConflictListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null) + setText(item.getNumProposals()); + else + setText(""); + } + }; + } + }); + column.setComparator(Comparator.comparing(e -> e.getStateHash().getNumProposals())); + conflictTableView.getColumns().add(1, column); + } +} diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index 6ea6467512d..ddb327875f7 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -297,7 +297,9 @@ public boolean noCapabilityRequiredOrCapabilityIsSupported(Proto msg) { } if (!result) - log.info("We did not send the message because the peer does not support our required capabilities. message={}, peers supportedCapabilities={}", msg, capabilities); + log.info("We did not send the message because the peer does not support our required capabilities. " + + "message={}, peer={}, peers supportedCapabilities={}", + msg, peersNodeAddressOptional, capabilities); return result; } diff --git a/p2p/src/main/resources/BlindVoteStore_BTC_DAO_TESTNET b/p2p/src/main/resources/BlindVoteStore_BTC_DAO_TESTNET index 9f04c091681..e8136c63054 100644 Binary files a/p2p/src/main/resources/BlindVoteStore_BTC_DAO_TESTNET and b/p2p/src/main/resources/BlindVoteStore_BTC_DAO_TESTNET differ diff --git a/p2p/src/main/resources/DaoStateStore2_BTC_DAO_TESTNET b/p2p/src/main/resources/DaoStateStore2_BTC_DAO_TESTNET new file mode 100644 index 00000000000..909f2dbc7e8 Binary files /dev/null and b/p2p/src/main/resources/DaoStateStore2_BTC_DAO_TESTNET differ diff --git a/p2p/src/main/resources/DaoStateStore_BTC_DAO_TESTNET b/p2p/src/main/resources/DaoStateStore_BTC_DAO_TESTNET deleted file mode 100644 index 7a1bc0eaee2..00000000000 Binary files a/p2p/src/main/resources/DaoStateStore_BTC_DAO_TESTNET and /dev/null differ diff --git a/p2p/src/main/resources/ProposalStore_BTC_DAO_TESTNET b/p2p/src/main/resources/ProposalStore_BTC_DAO_TESTNET index 67f9fd5759b..f80d555d880 100644 Binary files a/p2p/src/main/resources/ProposalStore_BTC_DAO_TESTNET and b/p2p/src/main/resources/ProposalStore_BTC_DAO_TESTNET differ