diff --git a/pom.xml b/pom.xml
index 4aeb5182..be4b89ae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,6 +14,9 @@
1.3.8
3.6
1.8
+ 2.6
+ 1.21
+ 1.9
1.2.2
28.1-jre
2.5.1
@@ -449,6 +452,21 @@
commons-text
${commons-text.version}
+
+ commons-io
+ commons-io
+ ${commons-io.version}
+
+
+ org.apache.commons
+ commons-compress
+ ${commons-compress.version}
+
+
+ org.tukaani
+ xz
+ ${xz.version}
+
io.druid
diff --git a/src/main/java/org/qortal/api/resource/BootstrapResource.java b/src/main/java/org/qortal/api/resource/BootstrapResource.java
new file mode 100644
index 00000000..fe2ed378
--- /dev/null
+++ b/src/main/java/org/qortal/api/resource/BootstrapResource.java
@@ -0,0 +1,84 @@
+package org.qortal.api.resource;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.api.ApiError;
+import org.qortal.api.ApiExceptionFactory;
+import org.qortal.api.Security;
+import org.qortal.repository.Bootstrap;
+import org.qortal.repository.DataException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.*;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+
+
+@Path("/bootstrap")
+@Tag(name = "Bootstrap")
+public class BootstrapResource {
+
+ private static final Logger LOGGER = LogManager.getLogger(BootstrapResource.class);
+
+ @Context
+ HttpServletRequest request;
+
+ @POST
+ @Path("/create")
+ @Operation(
+ summary = "Create bootstrap",
+ description = "Builds a bootstrap file for distribution",
+ responses = {
+ @ApiResponse(
+ description = "path to file on success, an exception on failure",
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
+ )
+ }
+ )
+ public String createBootstrap() {
+ Security.checkApiCallAllowed(request);
+
+ Bootstrap bootstrap = new Bootstrap();
+ if (!bootstrap.canBootstrap()) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
+ }
+
+ boolean isBlockchainValid = bootstrap.validateBlockchain();
+ if (!isBlockchainValid) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE);
+ }
+
+ try {
+ return bootstrap.create();
+
+ } catch (DataException | InterruptedException | IOException e) {
+ throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE);
+ }
+ }
+
+ @GET
+ @Path("/validate")
+ @Operation(
+ summary = "Validate blockchain",
+ description = "Useful to check database integrity prior to creating or after installing a bootstrap. " +
+ "This process is intensive and can take over an hour to run.",
+ responses = {
+ @ApiResponse(
+ description = "true if valid, false if invalid",
+ content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
+ )
+ }
+ )
+ public boolean validateBootstrap() {
+ Security.checkApiCallAllowed(request);
+
+ Bootstrap bootstrap = new Bootstrap();
+ return bootstrap.validateCompleteBlockchain();
+ }
+}
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index aee85131..98b8d4fd 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -4,10 +4,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import javax.xml.bind.JAXBContext;
@@ -27,11 +24,9 @@ import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.controller.Controller;
import org.qortal.data.block.BlockData;
import org.qortal.network.Network;
-import org.qortal.repository.BlockRepository;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.repository.RepositoryManager;
+import org.qortal.repository.*;
import org.qortal.settings.Settings;
+import org.qortal.utils.Base58;
import org.qortal.utils.StringLongMapXmlAdapter;
/**
@@ -506,23 +501,28 @@ public class BlockChain {
* @throws SQLException
*/
public static void validate() throws DataException {
+
+ BlockData chainTip;
try (final Repository repository = RepositoryManager.getRepository()) {
+ chainTip = repository.getBlockRepository().getLastBlock();
+ }
- boolean pruningEnabled = Settings.getInstance().isPruningEnabled();
- BlockData chainTip = repository.getBlockRepository().getLastBlock();
- boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1);
+ boolean pruningEnabled = Settings.getInstance().isPruningEnabled();
+ boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1);
- if (pruningEnabled && hasBlocks) {
- // Pruning is enabled and we have blocks, so it's possible that the genesis block has been pruned
- // It's best not to validate it, and there's no real need to
- }
- else {
- // Check first block is Genesis Block
- if (!isGenesisBlockValid()) {
- rebuildBlockchain();
- }
+ if (pruningEnabled && hasBlocks) {
+ // Pruning is enabled and we have blocks, so it's possible that the genesis block has been pruned
+ // It's best not to validate it, and there's no real need to
+ } else {
+ // Check first block is Genesis Block
+ if (!isGenesisBlockValid()) {
+ rebuildBlockchain();
}
+ }
+ // We need to create a new connection, as the previous repository and its connections may be been
+ // closed by rebuildBlockchain() if a bootstrap was applied
+ try (final Repository repository = RepositoryManager.getRepository()) {
repository.checkConsistency();
// Set the number of blocks to validate based on the pruned state of the chain
@@ -615,6 +615,14 @@ public class BlockChain {
}
private static void rebuildBlockchain() throws DataException {
+ boolean shouldBootstrap = Settings.getInstance().getBootstrap();
+ if (shouldBootstrap) {
+ // Settings indicate that we should apply a bootstrap rather than rebuilding and syncing from genesis
+ Bootstrap bootstrap = new Bootstrap();
+ bootstrap.startImport();
+ return;
+ }
+
// (Re)build repository
if (!RepositoryManager.wasPristineAtOpen())
RepositoryManager.rebuild();
diff --git a/src/main/java/org/qortal/repository/Bootstrap.java b/src/main/java/org/qortal/repository/Bootstrap.java
new file mode 100644
index 00000000..2289db5e
--- /dev/null
+++ b/src/main/java/org/qortal/repository/Bootstrap.java
@@ -0,0 +1,362 @@
+package org.qortal.repository;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.block.BlockChain;
+import org.qortal.controller.Controller;
+import org.qortal.data.account.MintingAccountData;
+import org.qortal.data.block.BlockData;
+import org.qortal.data.crosschain.TradeBotData;
+import org.qortal.repository.hsqldb.HSQLDBImportExport;
+import org.qortal.repository.hsqldb.HSQLDBRepository;
+import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
+import org.qortal.settings.Settings;
+import org.qortal.utils.NTP;
+import org.qortal.utils.SevenZ;
+
+import java.io.BufferedInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.concurrent.locks.ReentrantLock;
+
+
+public class Bootstrap {
+
+ private static final Logger LOGGER = LogManager.getLogger(Bootstrap.class);
+
+ /** The maximum number of untrimmed blocks allowed to be included in a bootstrap, beyond the trim threshold */
+ private static final int MAXIMUM_UNTRIMMED_BLOCKS = 100;
+
+ /** The maximum number of unpruned blocks allowed to be included in a bootstrap, beyond the prune threshold */
+ private static final int MAXIMUM_UNPRUNED_BLOCKS = 100;
+
+
+ public Bootstrap() {
+
+ }
+
+ /**
+ * canBootstrap()
+ * Performs basic initial checks to ensure everything is in order
+ * @return true if ready for bootstrap creation, or false if not
+ * All failure reasons are logged
+ */
+ public boolean canBootstrap() {
+ LOGGER.info("Checking repository state...");
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+
+ final boolean pruningEnabled = Settings.getInstance().isPruningEnabled();
+ final boolean archiveEnabled = Settings.getInstance().isArchiveEnabled();
+
+ // Avoid creating bootstraps from pruned nodes until officially supported
+ if (pruningEnabled) {
+ LOGGER.info("Creating bootstraps from top-only nodes isn't yet supported.");
+ // TODO: add support for top-only bootstraps
+ return false;
+ }
+
+ // Require that a block archive has been built
+ if (!archiveEnabled) {
+ LOGGER.info("Unable to bootstrap because the block archive isn't enabled. " +
+ "Set {\"archivedEnabled\": true} in settings.json to fix.");
+ return false;
+ }
+
+ // Make sure that the block archiver is up to date
+ boolean upToDate = BlockArchiveWriter.isArchiverUpToDate(repository);
+ if (!upToDate) {
+ LOGGER.info("Unable to bootstrap because the block archive isn't fully built yet.");
+ return false;
+ }
+
+ // Ensure that this database contains the ATStatesHeightIndex which was missing in some cases
+ boolean hasAtStatesHeightIndex = repository.getATRepository().hasAtStatesHeightIndex();
+ if (!hasAtStatesHeightIndex) {
+ LOGGER.info("Unable to bootstrap due to missing ATStatesHeightIndex. A re-sync from genesis is needed.");
+ return false;
+ }
+
+ // Ensure we have synced NTP time
+ if (NTP.getTime() == null) {
+ LOGGER.info("Unable to bootstrap because the node hasn't synced its time yet.");
+ return false;
+ }
+
+ // Ensure the chain is synced
+ final BlockData chainTip = Controller.getInstance().getChainTip();
+ final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
+ if (minLatestBlockTimestamp == null || chainTip.getTimestamp() < minLatestBlockTimestamp) {
+ LOGGER.info("Unable to bootstrap because the blockchain isn't fully synced.");
+ return false;
+ }
+
+ // FUTURE: ensure trim and prune settings are using default values
+
+ // Ensure that the online account signatures have been fully trimmed
+ final int accountsTrimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
+ final long accountsUpperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
+ final int accountsUpperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(accountsUpperTrimmableTimestamp);
+ final int accountsBlocksRemaining = accountsUpperTrimmableHeight - accountsTrimStartHeight;
+ if (accountsBlocksRemaining > MAXIMUM_UNTRIMMED_BLOCKS) {
+ LOGGER.info("Blockchain is not fully trimmed. Please allow the node to run for longer, " +
+ "then try again. Blocks remaining (online accounts signatures): {}", accountsBlocksRemaining);
+ return false;
+ }
+
+ // Ensure that the AT states data has been fully trimmed
+ final int atTrimStartHeight = repository.getATRepository().getAtTrimHeight();
+ final long atUpperTrimmableTimestamp = chainTip.getTimestamp() - Settings.getInstance().getAtStatesMaxLifetime();
+ final int atUpperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(atUpperTrimmableTimestamp);
+ final int atBlocksRemaining = atUpperTrimmableHeight - atTrimStartHeight;
+ if (atBlocksRemaining > MAXIMUM_UNTRIMMED_BLOCKS) {
+ LOGGER.info("Blockchain is not fully trimmed. Please allow the node to run for longer, " +
+ "then try again. Blocks remaining (AT states): {}", atBlocksRemaining);
+ return false;
+ }
+
+ // Ensure that blocks have been fully pruned
+ final int blockPruneStartHeight = repository.getBlockRepository().getBlockPruneHeight();
+ int blockUpperPrunableHeight = chainTip.getHeight() - Settings.getInstance().getPruneBlockLimit();
+ if (archiveEnabled) {
+ blockUpperPrunableHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1;
+ }
+ final int blocksPruneRemaining = blockUpperPrunableHeight - blockPruneStartHeight;
+ if (blocksPruneRemaining > MAXIMUM_UNPRUNED_BLOCKS) {
+ LOGGER.info("Blockchain is not fully pruned. Please allow the node to run for longer, " +
+ "then try again. Blocks remaining: {}", blocksPruneRemaining);
+ return false;
+ }
+
+ // Ensure that AT states have been fully pruned
+ final int atPruneStartHeight = repository.getATRepository().getAtPruneHeight();
+ int atUpperPrunableHeight = chainTip.getHeight() - Settings.getInstance().getPruneBlockLimit();
+ if (archiveEnabled) {
+ atUpperPrunableHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1;
+ }
+ final int atPruneRemaining = atUpperPrunableHeight - atPruneStartHeight;
+ if (atPruneRemaining > MAXIMUM_UNPRUNED_BLOCKS) {
+ LOGGER.info("Blockchain is not fully pruned. Please allow the node to run for longer, " +
+ "then try again. Blocks remaining (AT states): {}", atPruneRemaining);
+ return false;
+ }
+
+ LOGGER.info("Repository state checks passed");
+ return true;
+ }
+ catch (DataException e) {
+ LOGGER.info("Unable to create bootstrap: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * validateBlockchain
+ * Performs quick validation of recent blocks in blockchain, prior to creating a bootstrap
+ * @return true if valid, false if not
+ */
+ public boolean validateBlockchain() {
+ LOGGER.info("Validating blockchain...");
+
+ try {
+ BlockChain.validate();
+
+ LOGGER.info("Blockchain is valid");
+
+ return true;
+ } catch (DataException e) {
+ LOGGER.info("Blockchain validation failed: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * validateCompleteBlockchain
+ * Performs intensive validation of all blocks in blockchain
+ * @return true if valid, false if not
+ */
+ public boolean validateCompleteBlockchain() {
+ LOGGER.info("Validating blockchain...");
+
+ try {
+ // Perform basic startup validation
+ BlockChain.validate();
+
+ // Perform more intensive full-chain validation
+ BlockChain.validateAllBlocks();
+
+ LOGGER.info("Blockchain is valid");
+
+ return true;
+ } catch (DataException e) {
+ LOGGER.info("Blockchain validation failed: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ public String create() throws DataException, InterruptedException, IOException {
+ try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) {
+
+ LOGGER.info("Acquiring blockchain lock...");
+ ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
+ blockchainLock.lockInterruptibly();
+
+ Path inputPath = null;
+
+ try {
+
+ LOGGER.info("Exporting local data...");
+ repository.exportNodeLocalData();
+
+ LOGGER.info("Deleting trade bot states...");
+ List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
+ for (TradeBotData tradeBotData : allTradeBotData) {
+ repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
+ }
+
+ LOGGER.info("Deleting minting accounts...");
+ List mintingAccounts = repository.getAccountRepository().getMintingAccounts();
+ for (MintingAccountData mintingAccount : mintingAccounts) {
+ repository.getAccountRepository().delete(mintingAccount.getPrivateKey());
+ }
+
+ repository.saveChanges();
+
+ LOGGER.info("Performing repository maintenance...");
+ repository.performPeriodicMaintenance();
+
+ LOGGER.info("Creating bootstrap...");
+ repository.backup(true, "bootstrap");
+
+ LOGGER.info("Moving files to output directory...");
+ inputPath = Paths.get(Settings.getInstance().getRepositoryPath(), "bootstrap");
+ Path outputPath = Paths.get("bootstrap");
+ FileUtils.deleteDirectory(outputPath.toFile());
+
+ // Move the db backup to a "bootstrap" folder in the root directory
+ Files.move(inputPath, outputPath);
+
+ // Copy the archive folder to inside the bootstrap folder
+ FileUtils.copyDirectory(
+ Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toFile(),
+ Paths.get(outputPath.toString(), "archive").toFile()
+ );
+
+ LOGGER.info("Compressing...");
+ String fileName = "bootstrap.7z";
+ SevenZ.compress(fileName, outputPath.toFile());
+
+ // Return the path to the compressed bootstrap file
+ Path finalPath = Paths.get(outputPath.toString(), fileName);
+ return finalPath.toAbsolutePath().toString();
+
+ } finally {
+ LOGGER.info("Re-importing local data...");
+ Path exportPath = HSQLDBImportExport.getExportDirectory(false);
+ repository.importDataFromFile(Paths.get(exportPath.toString(), "TradeBotStates.json").toString());
+ repository.importDataFromFile(Paths.get(exportPath.toString(), "MintingAccounts.json").toString());
+
+ blockchainLock.unlock();
+
+ // Cleanup
+ if (inputPath != null) {
+ FileUtils.deleteDirectory(inputPath.toFile());
+ }
+ }
+ }
+ }
+
+ public void startImport() throws DataException {
+ Path path = null;
+ try {
+ Path tempDir = Files.createTempDirectory("qortal-bootstrap");
+ path = Paths.get(tempDir.toString(), "bootstrap.7z");
+
+ this.downloadToPath(path);
+ this.importFromPath(path);
+
+ } catch (InterruptedException | DataException | IOException e) {
+ throw new DataException(String.format("Unable to import bootstrap: %s", e.getMessage()));
+ }
+ finally {
+ if (path != null) {
+ try {
+ FileUtils.deleteDirectory(path.toFile());
+
+ } catch (IOException e) {
+ // Temp folder will be cleaned up by system anyway, so ignore this failure
+ }
+ }
+ }
+ }
+
+ private void downloadToPath(Path path) throws DataException {
+ String bootstrapUrl = "http://bootstrap.qortal.org/bootstrap.7z";
+
+ while (!Controller.isStopping()) {
+ try {
+ LOGGER.info("Downloading bootstrap...");
+ InputStream in = new URL(bootstrapUrl).openStream();
+ Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
+ break;
+
+ } catch (IOException e) {
+ LOGGER.info("Unable to download bootstrap: {}", e.getMessage());
+ LOGGER.info("Retrying in 5 minutes");
+
+ try {
+ Thread.sleep(5 * 60 * 1000L);
+ } catch (InterruptedException e2) {
+ break;
+ }
+ }
+ }
+
+ // It's best to throw an exception on all failures, even though we're most likely just stopping
+ throw new DataException("Unable to download bootstrap");
+ }
+
+ private void importFromPath(Path path) throws InterruptedException, DataException, IOException {
+
+ ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
+ blockchainLock.lockInterruptibly();
+
+ try {
+ LOGGER.info("Extracting bootstrap...");
+ Path input = path.toAbsolutePath();
+ Path output = path.getParent().toAbsolutePath();
+ SevenZ.decompress(input.toString(), output.toFile());
+
+ LOGGER.info("Stopping repository...");
+ RepositoryManager.closeRepositoryFactory();
+
+ Path inputPath = Paths.get("bootstrap");
+ Path outputPath = Paths.get(Settings.getInstance().getRepositoryPath());
+ if (!inputPath.toFile().exists()) {
+ throw new DataException("Extracted bootstrap doesn't exist");
+ }
+
+ // Move the "bootstrap" folder in place of the "db" folder
+ LOGGER.info("Moving files to output directory...");
+ FileUtils.deleteDirectory(outputPath.toFile());
+ Files.move(inputPath, outputPath);
+
+ LOGGER.info("Starting repository from bootstrap...");
+ RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
+ RepositoryManager.setRepositoryFactory(repositoryFactory);
+
+ }
+ finally {
+ blockchainLock.unlock();
+ }
+ }
+
+}
diff --git a/src/main/java/org/qortal/repository/RepositoryManager.java b/src/main/java/org/qortal/repository/RepositoryManager.java
index c392d213..480edc59 100644
--- a/src/main/java/org/qortal/repository/RepositoryManager.java
+++ b/src/main/java/org/qortal/repository/RepositoryManager.java
@@ -50,9 +50,9 @@ public abstract class RepositoryManager {
repositoryFactory = null;
}
- public static void backup(boolean quick) {
+ public static void backup(boolean quick, String name) {
try (final Repository repository = getRepository()) {
- repository.backup(quick);
+ repository.backup(quick, name);
} catch (DataException e) {
// Backup is best-effort so don't complain
}
diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java
index d284d59d..8e1ed51b 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -138,6 +138,10 @@ public class Settings {
private long archiveInterval = 7171L; // milliseconds
+ /** Whether to automatically bootstrap instead of syncing from genesis */
+ private boolean bootstrap = true;
+
+
// Peer-to-peer related
private boolean isTestNet = false;
/** Port number for inbound peer-to-peer connections. */
@@ -603,4 +607,9 @@ public class Settings {
return this.archiveInterval;
}
+
+ public boolean getBootstrap() {
+ return this.bootstrap;
+ }
+
}
diff --git a/src/main/java/org/qortal/utils/SevenZ.java b/src/main/java/org/qortal/utils/SevenZ.java
new file mode 100644
index 00000000..7af7ffc0
--- /dev/null
+++ b/src/main/java/org/qortal/utils/SevenZ.java
@@ -0,0 +1,77 @@
+//
+// Code originally written by memorynotfound
+// https://memorynotfound.com/java-7z-seven-zip-example-compress-decompress-file/
+// Modified Sept 2021 by Qortal Core dev team
+//
+
+package org.qortal.utils;
+
+import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
+import org.apache.commons.compress.archivers.sevenz.SevenZFile;
+import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;
+
+import java.io.*;
+
+public class SevenZ {
+
+ private SevenZ() {
+
+ }
+
+ public static void compress(String name, File... files) throws IOException {
+ try (SevenZOutputFile out = new SevenZOutputFile(new File(name))){
+ for (File file : files){
+ addToArchiveCompression(out, file, ".");
+ }
+ }
+ }
+
+ public static void decompress(String in, File destination) throws IOException {
+ SevenZFile sevenZFile = new SevenZFile(new File(in));
+ SevenZArchiveEntry entry;
+ while ((entry = sevenZFile.getNextEntry()) != null){
+ if (entry.isDirectory()){
+ continue;
+ }
+ File curfile = new File(destination, entry.getName());
+ File parent = curfile.getParentFile();
+ if (!parent.exists()) {
+ parent.mkdirs();
+ }
+
+ FileOutputStream out = new FileOutputStream(curfile);
+ byte[] b = new byte[8192];
+ int count = 0;
+ while ((count = sevenZFile.read(b)) > 0) {
+ out.write(b, 0, count);
+ }
+ out.close();
+ }
+ }
+
+ private static void addToArchiveCompression(SevenZOutputFile out, File file, String dir) throws IOException {
+ String name = dir + File.separator + file.getName();
+ if (file.isFile()){
+ SevenZArchiveEntry entry = out.createArchiveEntry(file, name);
+ out.putArchiveEntry(entry);
+
+ FileInputStream in = new FileInputStream(file);
+ byte[] b = new byte[8192];
+ int count = 0;
+ while ((count = in.read(b)) > 0) {
+ out.write(b, 0, count);
+ }
+ out.closeArchiveEntry();
+
+ } else if (file.isDirectory()) {
+ File[] children = file.listFiles();
+ if (children != null){
+ for (File child : children){
+ addToArchiveCompression(out, child, name);
+ }
+ }
+ } else {
+ System.out.println(file.getName() + " is not supported");
+ }
+ }
+}
diff --git a/src/test/resources/test-settings-v2-bitcoin-regtest.json b/src/test/resources/test-settings-v2-bitcoin-regtest.json
index f0a993e2..7f03b447 100644
--- a/src/test/resources/test-settings-v2-bitcoin-regtest.json
+++ b/src/test/resources/test-settings-v2-bitcoin-regtest.json
@@ -4,6 +4,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
diff --git a/src/test/resources/test-settings-v2-block-archive.json b/src/test/resources/test-settings-v2-block-archive.json
index b71b2679..7cac32b6 100644
--- a/src/test/resources/test-settings-v2-block-archive.json
+++ b/src/test/resources/test-settings-v2-block-archive.json
@@ -4,6 +4,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0,
diff --git a/src/test/resources/test-settings-v2-founder-rewards.json b/src/test/resources/test-settings-v2-founder-rewards.json
index b73544ea..fedd5de4 100644
--- a/src/test/resources/test-settings-v2-founder-rewards.json
+++ b/src/test/resources/test-settings-v2-founder-rewards.json
@@ -2,6 +2,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-founder-rewards.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
diff --git a/src/test/resources/test-settings-v2-leftover-reward.json b/src/test/resources/test-settings-v2-leftover-reward.json
index 5c87cc94..45f86ff3 100644
--- a/src/test/resources/test-settings-v2-leftover-reward.json
+++ b/src/test/resources/test-settings-v2-leftover-reward.json
@@ -2,6 +2,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-leftover-reward.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
diff --git a/src/test/resources/test-settings-v2-minting.json b/src/test/resources/test-settings-v2-minting.json
index abff27e3..c2522774 100644
--- a/src/test/resources/test-settings-v2-minting.json
+++ b/src/test/resources/test-settings-v2-minting.json
@@ -2,6 +2,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-minting.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
diff --git a/src/test/resources/test-settings-v2-qora-holder-extremes.json b/src/test/resources/test-settings-v2-qora-holder-extremes.json
index dbf55170..a4422562 100644
--- a/src/test/resources/test-settings-v2-qora-holder-extremes.json
+++ b/src/test/resources/test-settings-v2-qora-holder-extremes.json
@@ -2,6 +2,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder-extremes.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
diff --git a/src/test/resources/test-settings-v2-qora-holder.json b/src/test/resources/test-settings-v2-qora-holder.json
index c9b995a6..f8777ca1 100644
--- a/src/test/resources/test-settings-v2-qora-holder.json
+++ b/src/test/resources/test-settings-v2-qora-holder.json
@@ -2,6 +2,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
diff --git a/src/test/resources/test-settings-v2-reward-levels.json b/src/test/resources/test-settings-v2-reward-levels.json
index 4cc8de14..02a91d28 100644
--- a/src/test/resources/test-settings-v2-reward-levels.json
+++ b/src/test/resources/test-settings-v2-reward-levels.json
@@ -2,6 +2,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-reward-levels.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
diff --git a/src/test/resources/test-settings-v2-reward-scaling.json b/src/test/resources/test-settings-v2-reward-scaling.json
index e1958d63..87f77d44 100644
--- a/src/test/resources/test-settings-v2-reward-scaling.json
+++ b/src/test/resources/test-settings-v2-reward-scaling.json
@@ -2,6 +2,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-reward-scaling.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
diff --git a/src/test/resources/test-settings-v2.json b/src/test/resources/test-settings-v2.json
index 13c0a60f..4dfaeac1 100644
--- a/src/test/resources/test-settings-v2.json
+++ b/src/test/resources/test-settings-v2.json
@@ -4,6 +4,7 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2.json",
"exportPath": "qortal-backup-test",
+ "bootstrap": false,
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0