diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java index fb8040b8ab..b7b647d685 100644 --- a/common/src/main/java/bisq/common/config/Config.java +++ b/common/src/main/java/bisq/common/config/Config.java @@ -139,6 +139,7 @@ public class Config { public static final String BM_ORACLE_NODE_PRIV_KEY = "bmOracleNodePrivKey"; public static final String SEED_NODE_REPORTING_SERVER_URL = "seedNodeReportingServerUrl"; public static final String USE_TOR_FOR_BTC_MONITOR = "useTorForBtcMonitor"; + public static final String USE_FULL_MODE_DAO_MONITOR = "useFullModeDaoMonitor"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -239,6 +240,8 @@ public class Config { public final String bmOracleNodePrivKey; public final String seedNodeReportingServerUrl; public final boolean useTorForBtcMonitor; + public final boolean useFullModeDaoMonitor; + public final boolean useFullModeDaoMonitorSetExplicitly; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -716,7 +719,7 @@ public class Config { parser.accepts(DAO_NODE_API_PORT, "Dao node API port") .withRequiredArg() .ofType(Integer.class) - .defaultsTo(8082); + .defaultsTo(8081); ArgumentAcceptingOptionSpec isBmFullNode = parser.accepts(IS_BM_FULL_NODE, "Run as Burningman full node") @@ -746,6 +749,14 @@ public class Config { .ofType(Boolean.class) .defaultsTo(true); + ArgumentAcceptingOptionSpec useFullModeDaoMonitorOpt = + parser.accepts(USE_FULL_MODE_DAO_MONITOR, "If set to true full mode DAO monitor is activated. " + + "By that at each block during parsing the dao state hash is created, " + + "otherwise only after block parsing is complete and on new blocks.") + .withRequiredArg() + .ofType(Boolean.class) + .defaultsTo(false); + try { CompositeOptionSet options = new CompositeOptionSet(); @@ -873,6 +884,8 @@ public class Config { this.bmOracleNodePrivKey = options.valueOf(bmOracleNodePrivKey); this.seedNodeReportingServerUrl = options.valueOf(seedNodeReportingServerUrlOpt); this.useTorForBtcMonitor = options.valueOf(useTorForBtcMonitorOpt); + this.useFullModeDaoMonitor = options.valueOf(useFullModeDaoMonitorOpt); + this.useFullModeDaoMonitorSetExplicitly = options.has(useFullModeDaoMonitorOpt); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), diff --git a/core/src/main/java/bisq/core/dao/DaoModule.java b/core/src/main/java/bisq/core/dao/DaoModule.java index 3d10caedb8..7d622e512a 100644 --- a/core/src/main/java/bisq/core/dao/DaoModule.java +++ b/core/src/main/java/bisq/core/dao/DaoModule.java @@ -225,6 +225,7 @@ public class DaoModule extends AppModule { bindConstant().annotatedWith(named(Config.IS_BM_FULL_NODE)).to(config.isBmFullNode); bindConstant().annotatedWith(named(Config.BM_ORACLE_NODE_PUB_KEY)).to(config.bmOracleNodePubKey); bindConstant().annotatedWith(named(Config.BM_ORACLE_NODE_PRIV_KEY)).to(config.bmOracleNodePrivKey); + bindConstant().annotatedWith(named(Config.USE_FULL_MODE_DAO_MONITOR)).to(config.useFullModeDaoMonitor); } } 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 3c8b465777..b3fedc9702 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java +++ b/core/src/main/java/bisq/core/dao/monitoring/DaoStateMonitoringService.java @@ -203,7 +203,7 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe verifyCheckpoints(); } - log.info("ParseBlockChainComplete: Accumulated updateHashChain() calls for {} block took {} ms " + + log.info("ParseBlockChainComplete: Accumulated updateHashChain() calls for {} blocks took {} ms " + "({} ms in average / block)", numCalls, accumulatedDuration, @@ -370,12 +370,19 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe UserThread.runAfter(() -> daoStateNetworkService.broadcastMyStateHash(myDaoStateHash), delayInSec); } long duration = System.currentTimeMillis() - ts; - // We don't want to spam the output. We log accumulated time after parsing is completed. log.trace("updateHashChain for block {} took {} ms", block.getHeight(), duration); accumulatedDuration += duration; numCalls++; + + if (numCalls % 10 == 0) { + log.info("Accumulated updateHashChain() calls for {} blocks took {} ms " + + "({} ms in average / block)", + numCalls, + accumulatedDuration, + (int) ((double) accumulatedDuration / (double) numCalls)); + } listeners.forEach(Listener::onDaoStateBlockCreated); return Optional.of(daoStateBlock); } 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 index e2a624576c..a97da52b16 100644 --- a/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateHash.java +++ b/core/src/main/java/bisq/core/dao/monitoring/model/DaoStateHash.java @@ -26,7 +26,7 @@ import lombok.Getter; @Getter @EqualsAndHashCode(callSuper = true) public final class DaoStateHash extends StateHash { - // If we have built the hash by ourself opposed to that we got delivered the hash from seed nodes or resources + // If we have built the hash by ourselves opposed to that we got delivered the hash from seed nodes or resources private final boolean isSelfCreated; public DaoStateHash(int height, byte[] hash, boolean isSelfCreated) { 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 94fbc51bee..28f8a410b6 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateSnapshotService.java @@ -34,6 +34,7 @@ import bisq.common.config.Config; import bisq.common.util.GcUtil; import javax.inject.Inject; +import javax.inject.Named; import com.google.common.annotations.VisibleForTesting; @@ -50,7 +51,7 @@ import javax.annotation.Nullable; /** * Manages periodical snapshots of the DaoState. - * At startup we apply a snapshot if available. + * At startup, we apply a snapshot if available. * At each trigger height we persist the latest snapshot candidate and set the current daoState as new candidate. * The trigger height is determined by the SNAPSHOT_GRID. The latest persisted snapshot is min. the height of * SNAPSHOT_GRID old not less than 2 times the SNAPSHOT_GRID old. @@ -67,6 +68,7 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene private final BsqWalletService bsqWalletService; private final Preferences preferences; private final Config config; + private final boolean fullDaoNode; private protobuf.DaoState daoStateCandidate; private LinkedList hashChainCandidate = new LinkedList<>(); @@ -77,7 +79,7 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene @Nullable private Runnable resyncDaoStateFromResourcesHandler; private int daoRequiresRestartHandlerAttempts = 0; - private boolean readyForPersisting = true; + private boolean persistingBlockInProgress; private boolean isParseBlockChainComplete; private final List heightsOfLastAppliedSnapshots = new ArrayList<>(); @@ -93,7 +95,8 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene WalletsSetup walletsSetup, BsqWalletService bsqWalletService, Preferences preferences, - Config config) { + Config config, + @Named(Config.FULL_DAO_NODE) boolean fullDaoNode) { this.daoStateService = daoStateService; this.genesisTxInfo = genesisTxInfo; this.daoStateStorageService = daoStateStorageService; @@ -102,6 +105,7 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene this.bsqWalletService = bsqWalletService; this.preferences = preferences; this.config = config; + this.fullDaoNode = fullDaoNode; } @@ -122,6 +126,7 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene daoStateStorageService.shutDown(); } + /////////////////////////////////////////////////////////////////////////////////////////// // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// @@ -147,13 +152,16 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene @Override public void onDaoStateChanged(Block block) { // If we have isUseDaoMonitor activated we apply the hash and snapshots at each new block during initial parsing. - // Otherwise we do it only after the initial blockchain parsing is completed to not delay the parsing. + // Otherwise, we do it only after the initial blockchain parsing is completed to not delay the parsing. // In that case we get the missing hashes from the seed nodes. At any new block we do the hash calculation - // ourself and therefore get back confidence that our DAO state is in sync with the network. + // ourselves and therefore get back confidence that our DAO state is in sync with the network. if (preferences.isUseFullModeDaoMonitor() || isParseBlockChainComplete) { // We need to execute first the daoStateMonitoringService.createHashFromBlock to get the hash created daoStateMonitoringService.createHashFromBlock(block); maybeCreateSnapshot(block); + } else if (fullDaoNode) { + // If we run as full DAO node we want to create a snapshot at each trigger block. + maybeCreateSnapshot(block); } } @@ -161,7 +169,7 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene public void onParseBlockChainComplete() { isParseBlockChainComplete = true; - // In case we have dao monitoring deactivated we create the snapshot after we are completed with parsing + // In case we have dao monitoring deactivated we create the snapshot after we are completed with parsing, // and we got called back from daoStateMonitoringService once the hashes are created from peers data. if (!preferences.isUseFullModeDaoMonitor()) { // We register a callback handler once the daoStateMonitoringService has received the missing hashes from @@ -204,44 +212,51 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene // We need to process during batch processing as well to write snapshots during that process. public void maybeCreateSnapshot(Block block) { int chainHeight = block.getHeight(); + if (!isSnapshotHeight(chainHeight)) { + return; + } - // 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 = daoStateCandidate == null || - snapshotHeight != chainHeight; - if (isSnapshotHeight(chainHeight) && - !daoStateService.getBlocks().isEmpty() && - isHeightAtLeastGenesisHeight(daoStateService.getBlockHeightOfLastBlock()) && - noSnapshotCandidateOrDifferentHeight) { + if (isHeightBelowGenesisHeight(daoStateService.getBlockHeightOfLastBlock())) { + return; + } - // We protect to get called while we are not completed with persisting the daoState. This can take about - // 20 seconds and it is not expected that we get triggered another snapshot event in that period, but this - // check guards that we would skip such calls.. - if (!readyForPersisting) { - if (preferences.isUseFullModeDaoMonitor()) { - // In case we dont use isUseFullModeDaoMonitor we might called here too often as the parsing is much - // faster than the persistence and we likely create only 1 snapshot during initial parsing, so - // we log only if isUseFullModeDaoMonitor is true as then parsing is likely slower and we would - // expect that we do a snapshot at each trigger block. - log.info("We try to persist a daoState but the previous call has not completed yet. " + - "We ignore that call and skip that snapshot. " + - "Snapshot will be created at next snapshot height again. This is not to be expected with live " + - "blockchain data."); - } - return; + if (daoStateService.getBlocks().isEmpty()) { + log.error("No snapshot to be created as blocks are empty. This should never happen."); + return; + } + + if (daoStateCandidate != null && snapshotHeight == chainHeight) { + log.error("snapshotHeight is same as chainHeight. This should never happen. chainHeight={}", chainHeight); + return; + } + + // We protect to get called while we are not completed with persisting the daoState. This can take about + // 20 seconds, and it is not expected that we get triggered another snapshot event in that period, but this + // check guards that we would skip such calls. + if (persistingBlockInProgress) { + if (preferences.isUseFullModeDaoMonitor()) { + // In case we don't use isUseFullModeDaoMonitor we might get called here too often as the parsing is much + // faster than the persistence, and we likely create only 1 snapshot during initial parsing, so + // we log only if isUseFullModeDaoMonitor is true as then parsing is likely slower, and we would + // expect that we do a snapshot at each trigger block. + log.info("We try to persist a daoState but the previous call has not completed yet. " + + "We ignore that call and skip that snapshot. " + + "Snapshot will be created at next snapshot height again. This is not to be expected with live " + + "blockchain data."); } + return; + } - if (daoStateCandidate != null) { - persist(); - } else { - createSnapshot(); - } + if (daoStateCandidate != null) { + persist(); + } else { + createSnapshot(); } } private void persist() { long ts = System.currentTimeMillis(); - readyForPersisting = false; + persistingBlockInProgress = true; daoStateStorageService.requestPersistence(daoStateCandidate, blocksCandidate, hashChainCandidate, @@ -250,7 +265,7 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene snapshotHeight, System.currentTimeMillis() - ts); createSnapshot(); - readyForPersisting = true; + persistingBlockInProgress = false; }); } @@ -315,8 +330,7 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene return; } - if (!isHeightAtLeastGenesisHeight(chainHeightOfPersistedDaoState)) { - log.error("heightOfPersistedLastBlock is below genesis height. This should never happen."); + if (isHeightBelowGenesisHeight(chainHeightOfPersistedDaoState)) { return; } @@ -343,8 +357,12 @@ public class DaoStateSnapshotService implements DaoSetupService, DaoStateListene // Private /////////////////////////////////////////////////////////////////////////////////////////// - private boolean isHeightAtLeastGenesisHeight(int heightOfLastBlock) { - return heightOfLastBlock >= genesisTxInfo.getGenesisBlockHeight(); + private boolean isHeightBelowGenesisHeight(int height) { + boolean isHeightBelowGenesisHeight = height < genesisTxInfo.getGenesisBlockHeight(); + if (isHeightBelowGenesisHeight) { + log.error("height is below genesis height. This should never happen. height={}", height); + } + return isHeightBelowGenesisHeight; } private void resyncDaoStateFromResources() { diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index ab1e58dff2..79f387b368 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -171,7 +171,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid private final String btcNodesFromOptions, referralIdFromOptions, rpcUserFromOptions, rpcPwFromOptions; private final int blockNotifyPortFromOptions; - private final boolean fullDaoNodeFromOptions, fullAccountingNodeFromOptions; + private final boolean fullDaoNodeFromOptions, fullAccountingNodeFromOptions, useFullModeDaoMonitorFromOptions; @Getter private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode()); @@ -191,7 +191,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid @Named(Config.IS_BM_FULL_NODE) boolean fullAccountingNode, @Named(Config.RPC_USER) String rpcUser, @Named(Config.RPC_PASSWORD) String rpcPassword, - @Named(Config.RPC_BLOCK_NOTIFICATION_PORT) int rpcBlockNotificationPort) { + @Named(Config.RPC_BLOCK_NOTIFICATION_PORT) int rpcBlockNotificationPort, + @Named(Config.USE_FULL_MODE_DAO_MONITOR) boolean useFullModeDaoMonitor) { this.persistenceManager = persistenceManager; this.config = config; @@ -204,6 +205,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid this.rpcUserFromOptions = rpcUser; this.rpcPwFromOptions = rpcPassword; this.blockNotifyPortFromOptions = rpcBlockNotificationPort; + this.useFullModeDaoMonitorFromOptions = useFullModeDaoMonitor; useAnimationsProperty.addListener((ov) -> { prefPayload.setUseAnimations(useAnimationsProperty.get()); @@ -828,8 +830,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid } public void setUseFullModeDaoMonitor(boolean value) { - prefPayload.setUseFullModeDaoMonitor(value); - requestPersistence(); + // We only persist if we have not set the program argument + if (!config.useFullModeDaoMonitorSetExplicitly) { + prefPayload.setUseFullModeDaoMonitor(value); + requestPersistence(); + } } public void setUseBitcoinUrisInQrCodes(boolean value) { @@ -1031,6 +1036,14 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid return prefPayload.isFullBMAccountingNode(); } + public boolean isUseFullModeDaoMonitor() { + if (config.useFullModeDaoMonitorSetExplicitly) { + return useFullModeDaoMonitorFromOptions; + } else { + return prefPayload.isUseFullModeDaoMonitor(); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private @@ -1202,5 +1215,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid void setProcessBurningManAccountingData(boolean processBurningManAccountingData); void setFullBMAccountingNode(boolean isFullBMAccountingNode); + + boolean isUseFullModeDaoMonitor(); } } 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 e8180a2ec5..40d96b0943 100644 --- a/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java +++ b/core/src/test/java/bisq/core/dao/state/DaoStateSnapshotServiceTest.java @@ -41,7 +41,8 @@ public class DaoStateSnapshotServiceTest { null, null, null, - null); + null, + true); } @Test diff --git a/core/src/test/java/bisq/core/user/PreferencesTest.java b/core/src/test/java/bisq/core/user/PreferencesTest.java index aeb8af13d0..952d858051 100644 --- a/core/src/test/java/bisq/core/user/PreferencesTest.java +++ b/core/src/test/java/bisq/core/user/PreferencesTest.java @@ -69,7 +69,7 @@ public class PreferencesTest { LocalBitcoinNode localBitcoinNode = new LocalBitcoinNode(config); preferences = new Preferences( persistenceManager, config, null, localBitcoinNode, null, null, Config.DEFAULT_FULL_DAO_NODE, - false, null, null, Config.UNSPECIFIED_PORT); + false, null, null, Config.UNSPECIFIED_PORT, true); } @SuppressWarnings("unchecked") diff --git a/desktop/src/test/java/bisq/desktop/maker/PreferenceMakers.java b/desktop/src/test/java/bisq/desktop/maker/PreferenceMakers.java index d53f5d0860..4edf673eef 100644 --- a/desktop/src/test/java/bisq/desktop/maker/PreferenceMakers.java +++ b/desktop/src/test/java/bisq/desktop/maker/PreferenceMakers.java @@ -47,7 +47,7 @@ public class PreferenceMakers { lookup.valueOf(localBitcoinNode, new SameValueDonor<>(null)), lookup.valueOf(useTorFlagFromOptions, new SameValueDonor<>(null)), lookup.valueOf(referralID, new SameValueDonor<>(null)), - Config.DEFAULT_FULL_DAO_NODE, false, null, null, Config.UNSPECIFIED_PORT); + Config.DEFAULT_FULL_DAO_NODE, false, null, null, Config.UNSPECIFIED_PORT, false); public static final Preferences empty = make(a(Preferences)); }