mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-13 11:12:31 +00:00
Initial implementation of automatic bootstrapping
Currently supports block archive (i.e. full) bootstraps only. Still need to add support for top-only bootstraps.
This commit is contained in:
parent
de8e96cd75
commit
0a4479fe9e
18
pom.xml
18
pom.xml
@ -14,6 +14,9 @@
|
||||
<ciyam-at.version>1.3.8</ciyam-at.version>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
<commons-text.version>1.8</commons-text.version>
|
||||
<commons-io.version>2.6</commons-io.version>
|
||||
<commons-compress.version>1.21</commons-compress.version>
|
||||
<xz.version>1.9</xz.version>
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
<guava.version>28.1-jre</guava.version>
|
||||
<hsqldb.version>2.5.1</hsqldb.version>
|
||||
@ -449,6 +452,21 @@
|
||||
<artifactId>commons-text</artifactId>
|
||||
<version>${commons-text.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>${commons-compress.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.tukaani</groupId>
|
||||
<artifactId>xz</artifactId>
|
||||
<version>${xz.version}</version>
|
||||
</dependency>
|
||||
<!-- For bitset/bitmap compression -->
|
||||
<dependency>
|
||||
<groupId>io.druid</groupId>
|
||||
|
84
src/main/java/org/qortal/api/resource/BootstrapResource.java
Normal file
84
src/main/java/org/qortal/api/resource/BootstrapResource.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
362
src/main/java/org/qortal/repository/Bootstrap.java
Normal file
362
src/main/java/org/qortal/repository/Bootstrap.java
Normal file
@ -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<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
||||
for (TradeBotData tradeBotData : allTradeBotData) {
|
||||
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
|
||||
}
|
||||
|
||||
LOGGER.info("Deleting minting accounts...");
|
||||
List<MintingAccountData> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
77
src/main/java/org/qortal/utils/SevenZ.java
Normal file
77
src/main/java/org/qortal/utils/SevenZ.java
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user