diff --git a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java index 76a086fc3fb..d0846e11b54 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java @@ -17,10 +17,12 @@ package bisq.core.dao.monitoring; +import bisq.core.app.AppOptionKeys; import bisq.core.dao.DaoSetupService; import bisq.core.dao.monitoring.model.DaoStateBlock; import bisq.core.dao.monitoring.model.DaoStateHash; import bisq.core.dao.monitoring.model.UtxoMismatch; +import bisq.core.dao.monitoring.network.Checkpoint; import bisq.core.dao.monitoring.network.DaoStateNetworkService; import bisq.core.dao.monitoring.network.messages.GetDaoStateHashesRequest; import bisq.core.dao.monitoring.network.messages.NewDaoStateHashMessage; @@ -37,14 +39,21 @@ import bisq.common.UserThread; import bisq.common.crypto.Hash; +import bisq.common.storage.FileManager; +import bisq.common.storage.Storage; +import bisq.common.util.Utilities; import javax.inject.Inject; +import javax.inject.Named; import org.apache.commons.lang3.ArrayUtils; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.io.File; + +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -80,6 +89,8 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe public interface Listener { void onChangeAfterBatchProcessing(); + + void onCheckpointFail(); } private final DaoStateService daoStateService; @@ -101,6 +112,13 @@ public interface Listener { @Getter private ObservableList utxoMismatches = FXCollections.observableArrayList(); + private List checkpoints = Arrays.asList( + new Checkpoint(586920, Utilities.decodeFromHex("523aaad4e760f6ac6196fec1b3ec9a2f42e5b272")) + ); + private boolean checkpointFailed; + private boolean ignoreDevMsg; + + private final File storageDir; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -110,10 +128,14 @@ public interface Listener { public DaoStateMonitoringService(DaoStateService daoStateService, DaoStateNetworkService daoStateNetworkService, GenesisTxInfo genesisTxInfo, - SeedNodeRepository seedNodeRepository) { + SeedNodeRepository seedNodeRepository, + @Named(Storage.STORAGE_DIR) File storageDir, + @Named(AppOptionKeys.IGNORE_DEV_MSG_KEY) boolean ignoreDevMsg) { this.daoStateService = daoStateService; this.daoStateNetworkService = daoStateNetworkService; this.genesisTxInfo = genesisTxInfo; + this.storageDir = storageDir; + this.ignoreDevMsg = ignoreDevMsg; seedNodeAddresses = seedNodeRepository.getSeedNodeAddresses().stream() .map(NodeAddress::getFullAddress) .collect(Collectors.toSet()); @@ -150,6 +172,10 @@ public void onParseBlockChainComplete() { // We wait for processing messages until we have completed batch processing int fromHeight = daoStateService.getChainHeight() - 10; daoStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight); + + if (!ignoreDevMsg) { + verifyCheckpoints(); + } } @Override @@ -167,6 +193,7 @@ public void onDaoStateChanged(Block block) { } } + /////////////////////////////////////////////////////////////////////////////////////////// // StateNetworkService.Listener /////////////////////////////////////////////////////////////////////////////////////////// @@ -291,7 +318,8 @@ private void updateHashChain(Block block) { } } - private boolean processPeersDaoStateHash(DaoStateHash daoStateHash, Optional peersNodeAddress, boolean notifyListeners) { + private boolean processPeersDaoStateHash(DaoStateHash daoStateHash, Optional peersNodeAddress, + boolean notifyListeners) { AtomicBoolean changed = new AtomicBoolean(false); AtomicBoolean inConflictWithNonSeedNode = new AtomicBoolean(this.isInConflictWithNonSeedNode); AtomicBoolean inConflictWithSeedNode = new AtomicBoolean(this.isInConflictWithSeedNode); @@ -338,4 +366,49 @@ else if (this.isInConflictWithNonSeedNode) return changed.get(); } + + private void verifyCheckpoints() { + // Checkpoint + checkpoints.forEach(checkpoint -> daoStateHashChain.stream() + .filter(daoStateHash -> daoStateHash.getHeight() == checkpoint.getHeight()) + .findAny() + .ifPresent(daoStateHash -> { + if (Arrays.equals(daoStateHash.getHash(), checkpoint.getHash())) { + log.info("Passed checkpoint {}", checkpoint.toString()); + } else { + if (checkpointFailed) { + return; + } + checkpointFailed = true; + try { + // Delete state and stop + removeFile("DaoStateStore"); + removeFile("BlindVoteStore"); + removeFile("ProposalStore"); + removeFile("TempProposalStore"); + + listeners.forEach(Listener::onCheckpointFail); + log.error("Failed checkpoint {}", checkpoint.toString()); + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.toString()); + } + } + })); + } + + private void removeFile(String storeName) { + long currentTime = System.currentTimeMillis(); + String newFileName = storeName + "_" + currentTime; + String backupDirName = "out_of_sync_dao_data"; + File corrupted = new File(storageDir, storeName); + try { + if (corrupted.exists()) { + FileManager.removeAndBackupFile(storageDir, corrupted, newFileName, backupDirName); + } + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.toString()); + } + } } diff --git a/core/src/main/java/bisq/core/dao/monitoring/network/Checkpoint.java b/core/src/main/java/bisq/core/dao/monitoring/network/Checkpoint.java new file mode 100644 index 00000000000..911db2e15ac --- /dev/null +++ b/core/src/main/java/bisq/core/dao/monitoring/network/Checkpoint.java @@ -0,0 +1,45 @@ +/* + * 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.common.util.Utilities; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public class Checkpoint { + final int height; + final byte[] hash; + @Setter + boolean passed; + + public Checkpoint(int height, byte[] hash) { + this.height = height; + this.hash = hash; + } + + @Override + public String toString() { + return "Checkpoint {" + + "\n height=" + height + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } + +} diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index c29d0c4a5e7..d5b7a1169a7 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1971,6 +1971,8 @@ dao.monitor.daoState.utxoConflicts=UTXO conflicts dao.monitor.daoState.utxoConflicts.blockHeight=Block height: {0} dao.monitor.daoState.utxoConflicts.sumUtxo=Sum of all UTXO: {0} BSQ dao.monitor.daoState.utxoConflicts.sumBsq=Sum of all BSQ: {0} BSQ +dao.monitor.daoState.checkpoint.popup=DAO state is not in sync with the network. \ + After restart the DAO state will resync. dao.monitor.proposal.headline=Proposals state dao.monitor.proposal.table.headline=Chain of proposal state hashes diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index d0315f7676e..8c661bd9bea 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -39,6 +39,7 @@ import bisq.desktop.main.settings.SettingsView; import bisq.desktop.util.Transitions; +import bisq.core.dao.monitoring.DaoStateMonitoringService; import bisq.core.exceptions.BisqException; import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; @@ -104,7 +105,8 @@ @FxmlView @Slf4j -public class MainView extends InitializableView { +public class MainView extends InitializableView + implements DaoStateMonitoringService.Listener { // If after 30 sec we have not got connected we show "open network settings" button private final static int SHOW_TOR_SETTINGS_DELAY_SEC = 90; private Label versionLabel; @@ -150,18 +152,21 @@ public static void removeEffect() { private ProgressBar btcSyncIndicator, p2pNetworkProgressBar; private Label btcSplashInfo; private Popup p2PNetworkWarnMsgPopup, btcNetworkWarnMsgPopup; + private final DaoStateMonitoringService daoStateMonitoringService; @Inject public MainView(MainViewModel model, CachingViewLoader viewLoader, Navigation navigation, Transitions transitions, - BSFormatter formatter) { + BSFormatter formatter, + DaoStateMonitoringService daoStateMonitoringService) { super(model); this.viewLoader = viewLoader; this.navigation = navigation; this.formatter = formatter; MainView.transitions = transitions; + this.daoStateMonitoringService = daoStateMonitoringService; } @Override @@ -387,10 +392,33 @@ protected Tooltip computeValue() { } }); + daoStateMonitoringService.addListener(this); + // Delay a bit to give time for rendering the splash screen UserThread.execute(() -> onUiReadyHandler.run()); } + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateMonitoringService.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onChangeAfterBatchProcessing() { + } + + @Override + public void onCheckpointFail() { + new Popup<>().attention(Res.get("dao.monitor.daoState.checkpoint.popup")) + .useShutDownButton() + .hideCloseButton() + .show(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Helpers + /////////////////////////////////////////////////////////////////////////////////////////// + @NotNull private Separator getNavigationSeparator() { final Separator separator = new Separator(Orientation.VERTICAL); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java index 6f07846ff5d..1249be02fa8 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/daostate/DaoStateMonitorView.java @@ -117,6 +117,9 @@ public void onChangeAfterBatchProcessing() { } } + @Override + public void onCheckpointFail() { + } /////////////////////////////////////////////////////////////////////////////////////////// // Implementation abstract methods