mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-30 21:51:26 +00:00
Compare commits
67 Commits
pruning-mo
...
block-arch
Author | SHA1 | Date | |
---|---|---|---|
|
656896d16f | ||
|
19bf8afece | ||
|
841b6c4ddf | ||
|
4c171df848 | ||
|
1f79d88840 | ||
|
6ee7e9d731 | ||
|
4856223838 | ||
|
74ea2a847d | ||
|
9813dde3d9 | ||
|
fea7b62b9c | ||
|
37e03bf2bb | ||
|
5656de79a2 | ||
|
70c6048cc1 | ||
|
87595fd704 | ||
|
dc030a42bb | ||
|
89283ed179 | ||
|
64e8a05a9f | ||
|
676320586a | ||
|
734fa51806 | ||
|
f056ecc8d8 | ||
|
1a722c1517 | ||
|
44607ba6a4 | ||
|
01d66212da | ||
|
925e10b19b | ||
|
1b4c75a76e | ||
|
3400e36ac4 | ||
|
78e2ae4f36 | ||
|
957944f6a5 | ||
|
9eab500e2c | ||
|
573f4675a1 | ||
|
e6bde3e1f4 | ||
|
5869174021 | ||
|
449761b6ca | ||
|
39d5ce19e2 | ||
|
3b156bc5c9 | ||
|
a4f5124b61 | ||
|
47a34c2f54 | ||
|
8a7446fb40 | ||
|
705e7d1cf1 | ||
|
44a90b4e12 | ||
|
54e5a65cf0 | ||
|
06a2c380bd | ||
|
33ac1fed2a | ||
|
cc65a7cd11 | ||
|
d600a54034 | ||
|
ba06225b01 | ||
|
ce60ab8e00 | ||
|
14f6fd19ef | ||
|
1d8351f921 | ||
|
6a55b052f5 | ||
|
2a36b83dea | ||
|
14acc4feb9 | ||
|
0657ca2969 | ||
|
e90c3a78d1 | ||
|
63c9bc5c1c | ||
|
a6bbc81962 | ||
|
b800fb5846 | ||
|
172a629da3 | ||
|
6d1f7b36a7 | ||
|
673ee4aeed | ||
|
25b787f6f2 | ||
|
6b74ef77e6 | ||
|
278201e87c | ||
|
703cdfe174 | ||
|
02988989ad | ||
|
25c17d3704 | ||
|
9973fe4326 |
@@ -14,6 +14,8 @@ import java.security.SecureRandom;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
@@ -50,6 +52,8 @@ import org.qortal.settings.Settings;
|
||||
|
||||
public class ApiService {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ApiService.class);
|
||||
|
||||
private static ApiService instance;
|
||||
|
||||
private final ResourceConfig config;
|
||||
@@ -203,6 +207,9 @@ public class ApiService {
|
||||
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
|
||||
context.addServlet(PresenceWebSocket.class, "/websockets/presence");
|
||||
|
||||
// Warn about API security if needed
|
||||
this.checkApiSecurity();
|
||||
|
||||
// Start server
|
||||
this.server.start();
|
||||
} catch (Exception e) {
|
||||
@@ -222,4 +229,23 @@ public class ApiService {
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
private void checkApiSecurity() {
|
||||
// Warn about API security if needed
|
||||
boolean allConnectionsAllowed = false;
|
||||
if (Settings.getInstance().isApiKeyDisabled()) {
|
||||
for (String pattern : Settings.getInstance().getApiWhitelist()) {
|
||||
if (pattern.startsWith("0.0.0.0/") || pattern.startsWith("::/") || pattern.endsWith("/0")) {
|
||||
allConnectionsAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (allConnectionsAllowed) {
|
||||
LOGGER.warn("Warning: API key validation is currently disabled, and the API whitelist " +
|
||||
"is allowing all connections. This can be a security risk.");
|
||||
LOGGER.warn("To fix, set the apiKeyDisabled setting to false, or allow only specific local " +
|
||||
"IP addresses using the apiWhitelist setting.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,6 +12,11 @@ public abstract class Security {
|
||||
public static final String API_KEY_HEADER = "X-API-KEY";
|
||||
|
||||
public static void checkApiCallAllowed(HttpServletRequest request) {
|
||||
// If API key checking has been disabled, we will allow the request in all cases
|
||||
boolean isApiKeyDisabled = Settings.getInstance().isApiKeyDisabled();
|
||||
if (isApiKeyDisabled)
|
||||
return;
|
||||
|
||||
String expectedApiKey = Settings.getInstance().getApiKey();
|
||||
String passedApiKey = request.getHeader(API_KEY_HEADER);
|
||||
|
||||
|
@@ -35,6 +35,7 @@ import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.appender.RollingFileAppender;
|
||||
import org.qortal.account.Account;
|
||||
@@ -67,6 +68,8 @@ import com.google.common.collect.Lists;
|
||||
@Tag(name = "Admin")
|
||||
public class AdminResource {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(AdminResource.class);
|
||||
|
||||
private static final int MAX_LOG_LINES = 500;
|
||||
|
||||
@Context
|
||||
@@ -459,6 +462,23 @@ public class AdminResource {
|
||||
if (targetHeight <= 0 || targetHeight > Controller.getInstance().getChainHeight())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
|
||||
// Make sure we're not orphaning as far back as the archived blocks
|
||||
// FUTURE: we could support this by first importing earlier blocks from the archive
|
||||
if (Settings.getInstance().isPruningEnabled() ||
|
||||
Settings.getInstance().isArchiveEnabled()) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Find the first unarchived block
|
||||
int oldestBlock = repository.getBlockArchiveRepository().getBlockArchiveHeight();
|
||||
// Add some extra blocks just in case we're currently archiving/pruning
|
||||
oldestBlock += 100;
|
||||
if (targetHeight <= oldestBlock) {
|
||||
LOGGER.info("Unable to orphan beyond block {} because it is archived", oldestBlock);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (BlockChain.orphan(targetHeight))
|
||||
return "true";
|
||||
else
|
||||
|
@@ -15,6 +15,8 @@ import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -33,11 +35,13 @@ import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockMintingInfo;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockArchiveReader;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -81,11 +85,19 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
if (blockData != null) {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
return blockData;
|
||||
// Not found, so try the block archive
|
||||
blockData = repository.getBlockArchiveRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -116,16 +128,24 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
if (blockData != null) {
|
||||
Block block = new Block(repository, blockData);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
return Base58.encode(bytes.toByteArray());
|
||||
}
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
return Base58.encode(bytes.toByteArray());
|
||||
// Not found, so try the block archive
|
||||
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, repository);
|
||||
if (bytes != null) {
|
||||
return Base58.encode(bytes);
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
|
||||
} catch (DataException | IOException e) {
|
||||
@@ -170,8 +190,12 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0)
|
||||
// Check if the block exists in either the database or archive
|
||||
if (repository.getBlockRepository().getHeightFromSignature(signature) == 0 &&
|
||||
repository.getBlockArchiveRepository().getHeightFromSignature(signature) == 0) {
|
||||
// Not found in either the database or archive
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
@@ -200,7 +224,19 @@ public class BlocksResource {
|
||||
})
|
||||
public BlockData getFirstBlock() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().fromHeight(1);
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(1);
|
||||
if (blockData != null) {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
// Try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(1);
|
||||
if (blockData != null) {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -262,17 +298,28 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
BlockData childBlockData = null;
|
||||
|
||||
// Check if block exists in database
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
return repository.getBlockRepository().fromReference(signature);
|
||||
}
|
||||
|
||||
// Check block exists
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
BlockData childBlockData = repository.getBlockRepository().fromReference(signature);
|
||||
// Not found, so try the archive
|
||||
// This also checks that the parent block exists
|
||||
// It will return null if either the parent or child don't exit
|
||||
childBlockData = repository.getBlockArchiveRepository().fromReference(signature);
|
||||
|
||||
// Check child block exists
|
||||
if (childBlockData == null)
|
||||
if (childBlockData == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
// Check child block's reference matches the supplied signature
|
||||
if (!Arrays.equals(childBlockData.getReference(), signature)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
return childBlockData;
|
||||
} catch (DataException e) {
|
||||
@@ -338,13 +385,20 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Firstly check the database
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
return blockData.getHeight();
|
||||
}
|
||||
|
||||
// Check block exists
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
// Not found, so try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
return blockData.getHeight();
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData.getHeight();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -371,11 +425,20 @@ public class BlocksResource {
|
||||
})
|
||||
public BlockData getByHeight(@PathParam("height") int height) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Firstly check the database
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
if (blockData != null) {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
// Not found, so try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
if (blockData != null) {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
return blockData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -402,12 +465,31 @@ public class BlocksResource {
|
||||
})
|
||||
public BlockMintingInfo getBlockMintingInfoByHeight(@PathParam("height") int height) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Try the database
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
if (blockData == null) {
|
||||
|
||||
// Not found, so try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
if (blockData == null) {
|
||||
|
||||
// Still not found
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
BlockData parentBlockData = repository.getBlockRepository().fromSignature(blockData.getReference());
|
||||
if (parentBlockData == null) {
|
||||
// Parent block not found - try the archive
|
||||
parentBlockData = repository.getBlockArchiveRepository().fromSignature(blockData.getReference());
|
||||
if (parentBlockData == null) {
|
||||
|
||||
// Still not found
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
|
||||
if (minterLevel == 0)
|
||||
// This may be unavailable when requesting a trimmed block
|
||||
@@ -454,13 +536,26 @@ public class BlocksResource {
|
||||
})
|
||||
public BlockData getByTimestamp(@PathParam("timestamp") long timestamp) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int height = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
if (height == 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
BlockData blockData = null;
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
// Try the Blocks table
|
||||
int height = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
|
||||
if (height > 0) {
|
||||
// Found match in Blocks table
|
||||
return repository.getBlockRepository().fromHeight(height);
|
||||
}
|
||||
|
||||
// Not found in Blocks table, so try the archive
|
||||
height = repository.getBlockArchiveRepository().getHeightFromTimestamp(timestamp);
|
||||
if (height > 0) {
|
||||
// Found match in archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
}
|
||||
|
||||
// Ensure block exists
|
||||
if (blockData == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
}
|
||||
|
||||
return blockData;
|
||||
} catch (DataException e) {
|
||||
@@ -497,9 +592,14 @@ public class BlocksResource {
|
||||
|
||||
for (/* count already set */; count > 0; --count, ++height) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
if (blockData == null)
|
||||
// Run out of blocks!
|
||||
break;
|
||||
if (blockData == null) {
|
||||
// Not found - try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(height);
|
||||
if (blockData == null) {
|
||||
// Run out of blocks!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
blocks.add(blockData);
|
||||
}
|
||||
@@ -544,7 +644,29 @@ public class BlocksResource {
|
||||
if (accountData == null || accountData.getPublicKey() == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.PUBLIC_KEY_NOT_FOUND);
|
||||
|
||||
return repository.getBlockRepository().getBlockSummariesBySigner(accountData.getPublicKey(), limit, offset, reverse);
|
||||
|
||||
List<BlockSummaryData> summaries = repository.getBlockRepository()
|
||||
.getBlockSummariesBySigner(accountData.getPublicKey(), limit, offset, reverse);
|
||||
|
||||
// Add any from the archive
|
||||
List<BlockSummaryData> archivedSummaries = repository.getBlockArchiveRepository()
|
||||
.getBlockSummariesBySigner(accountData.getPublicKey(), limit, offset, reverse);
|
||||
if (archivedSummaries != null && !archivedSummaries.isEmpty()) {
|
||||
summaries.addAll(archivedSummaries);
|
||||
}
|
||||
else {
|
||||
summaries = archivedSummaries;
|
||||
}
|
||||
|
||||
// Sort the results (because they may have been obtained from two places)
|
||||
if (reverse != null && reverse) {
|
||||
summaries.sort((s1, s2) -> Integer.valueOf(s2.getHeight()).compareTo(Integer.valueOf(s1.getHeight())));
|
||||
}
|
||||
else {
|
||||
summaries.sort(Comparator.comparing(s -> Integer.valueOf(s.getHeight())));
|
||||
}
|
||||
|
||||
return summaries;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -580,7 +702,8 @@ public class BlocksResource {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
return repository.getBlockRepository().getBlockSigners(addresses, limit, offset, reverse);
|
||||
// This method pulls data from both Blocks and BlockArchive, so no need to query serparately
|
||||
return repository.getBlockArchiveRepository().getBlockSigners(addresses, limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -620,7 +743,76 @@ public class BlocksResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getBlockRepository().getBlockSummaries(startHeight, endHeight, count);
|
||||
|
||||
/*
|
||||
* start end count result
|
||||
* 10 40 null blocks 10 to 39 (excludes end block, ignore count)
|
||||
*
|
||||
* null null null blocks 1 to 50 (assume count=50, maybe start=1)
|
||||
* 30 null null blocks 30 to 79 (assume count=50)
|
||||
* 30 null 10 blocks 30 to 39
|
||||
*
|
||||
* null null 50 last 50 blocks? so if max(blocks.height) is 200, then blocks 151 to 200
|
||||
* null 200 null blocks 150 to 199 (excludes end block, assume count=50)
|
||||
* null 200 10 blocks 190 to 199 (excludes end block)
|
||||
*/
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
// Use the latest X blocks if only a count is specified
|
||||
if (startHeight == null && endHeight == null && count != null) {
|
||||
BlockData chainTip = repository.getBlockRepository().getLastBlock();
|
||||
startHeight = chainTip.getHeight() - count;
|
||||
endHeight = chainTip.getHeight();
|
||||
}
|
||||
|
||||
// ... otherwise default the start height to 1
|
||||
if (startHeight == null && endHeight == null) {
|
||||
startHeight = 1;
|
||||
}
|
||||
|
||||
// Default the count to 50
|
||||
if (count == null) {
|
||||
count = 50;
|
||||
}
|
||||
|
||||
// If both a start and end height exist, ignore the count
|
||||
if (startHeight != null && endHeight != null) {
|
||||
if (startHeight > 0 && endHeight > 0) {
|
||||
count = Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
// Derive start height from end height if missing
|
||||
if (startHeight == null || startHeight == 0) {
|
||||
if (endHeight != null && endHeight > 0) {
|
||||
if (count != null) {
|
||||
startHeight = endHeight - count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (/* count already set */; count > 0; --count, ++startHeight) {
|
||||
if (endHeight != null && startHeight >= endHeight) {
|
||||
break;
|
||||
}
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(startHeight);
|
||||
if (blockData == null) {
|
||||
// Not found - try the archive
|
||||
blockData = repository.getBlockArchiveRepository().fromHeight(startHeight);
|
||||
if (blockData == null) {
|
||||
// Run out of blocks!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (blockData != null) {
|
||||
BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
|
||||
blockSummaries.add(blockSummaryData);
|
||||
}
|
||||
}
|
||||
|
||||
return blockSummaries;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
@@ -1092,9 +1092,14 @@ public class Block {
|
||||
// Create repository savepoint here so we can rollback to it after testing transactions
|
||||
repository.setSavepoint();
|
||||
|
||||
if (this.blockData.getHeight() == 212937)
|
||||
if (this.blockData.getHeight() == 212937) {
|
||||
// Apply fix for block 212937 but fix will be rolled back before we exit method
|
||||
Block212937.processFix(this);
|
||||
}
|
||||
else if (InvalidNameRegistrationBlocks.isAffectedBlock(this.blockData.getHeight())) {
|
||||
// Apply fix for affected name registration blocks, but fix will be rolled back before we exit method
|
||||
InvalidNameRegistrationBlocks.processFix(this);
|
||||
}
|
||||
|
||||
for (Transaction transaction : this.getTransactions()) {
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
@@ -1133,7 +1138,7 @@ public class Block {
|
||||
// Check transaction can even be processed
|
||||
validationResult = transaction.isProcessable();
|
||||
if (validationResult != Transaction.ValidationResult.OK) {
|
||||
LOGGER.debug(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name()));
|
||||
LOGGER.info(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name()));
|
||||
return ValidationResult.TRANSACTION_INVALID;
|
||||
}
|
||||
|
||||
@@ -1282,6 +1287,21 @@ public class Block {
|
||||
return mintingAccount.canMint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-process block, and its transactions.
|
||||
* This allows for any database integrity checks prior to validation.
|
||||
* This is called before isValid() and process()
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public void preProcess() throws DataException {
|
||||
List<Transaction> blocksTransactions = this.getTransactions();
|
||||
|
||||
for (Transaction transaction : blocksTransactions) {
|
||||
transaction.preProcess();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process block, and its transactions, adding them to the blockchain.
|
||||
*
|
||||
|
@@ -0,0 +1,114 @@
|
||||
package org.qortal.block;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.naming.Name;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Invalid Name Registration Blocks
|
||||
* <p>
|
||||
* A node minted a version of block 535658 that contained one transaction:
|
||||
* a REGISTER_NAME transaction that attempted to register a name that was already registered.
|
||||
* <p>
|
||||
* This invalid transaction made block 535658 (rightly) invalid to several nodes,
|
||||
* which refused to use that block.
|
||||
* However, it seems there were no other nodes minting an alternative, valid block at that time
|
||||
* and so the chain stalled for several nodes in the network.
|
||||
* <p>
|
||||
* Additionally, the invalid block 535658 affected all new installations, regardless of whether
|
||||
* they synchronized from scratch (block 1) or used an 'official release' bootstrap.
|
||||
* <p>
|
||||
* The diagnosis found the following:
|
||||
* - The original problem occurred in block 535205 where for some unknown reason many nodes didn't
|
||||
* add the name from a REGISTER_NAME transaction to their Names table.
|
||||
* - As a result, those nodes had a corrupt db, because they weren't holding a record of the name.
|
||||
* - This invalid db then caused them to treat a candidate for block 535658 as valid when it
|
||||
* should have been invalid.
|
||||
* - As such, the chain continued on with a technically invalid block in it, for a subset of the network
|
||||
* <p>
|
||||
* As with block 212937, there were three options, but the only feasible one was to apply edits to block
|
||||
* 535658 to make it valid. There were several cross-chain trades completed after this block, so doing
|
||||
* any kind of rollback was out of the question.
|
||||
* <p>
|
||||
* To complicate things further, a custom data field was used for the first REGISTER_NAME transaction,
|
||||
* and the default data field was used for the second. So it was important that all nodes ended up with
|
||||
* the exact same data regardless of how they arrived there.
|
||||
* <p>
|
||||
* The invalid block 535658 signature is: <tt>3oiuDhok...NdXvCLEV</tt>.
|
||||
* <p>
|
||||
* The invalid transaction in block 212937 is:
|
||||
* <p>
|
||||
* <code><pre>
|
||||
{
|
||||
"type": "REGISTER_NAME",
|
||||
"timestamp": 1630739437517,
|
||||
"reference": "4peRechwSPxP6UkRj9Y8ox9YxkWb34sWk5zyMc1WyMxEsACxD4Gmm7LZVsQ6Skpze8QCSBMZasvEZg6RgdqkyADW",
|
||||
"fee": "0.00100000",
|
||||
"signature": "2t1CryCog8KPDBarzY5fDCKu499nfnUcGrz4Lz4w5wNb5nWqm7y126P48dChYY7huhufcBV3RJPkgKP4Ywxc1gXx",
|
||||
"txGroupId": 0,
|
||||
"blockHeight": 535658,
|
||||
"approvalStatus": "NOT_REQUIRED",
|
||||
"creatorAddress": "Qbx9ojxv7XNi1xDMWzzw7xDvd1zYW6SKFB",
|
||||
"registrantPublicKey": "HJqGEf6cW695Xun4ydhkB2excGFwsDxznhNCRHZStyyx",
|
||||
"name": "Qplay",
|
||||
"data": "Registered Name on the Qortal Chain"
|
||||
}
|
||||
</pre></code>
|
||||
* <p>
|
||||
* Account <tt>Qbx9ojxv7XNi1xDMWzzw7xDvd1zYW6SKFB</tt> attempted to register the name <tt>Qplay</tt>
|
||||
* when they had already registered it 12 hours before in block <tt>535205</tt>.
|
||||
* <p>
|
||||
* However, on the broken DB nodes, their Names table was missing a record for the `Qplay` name
|
||||
* which was sufficient to make the transaction valid.
|
||||
*
|
||||
* This problem then occurred two more times, in blocks 536140 and 541334
|
||||
* To reduce duplication, I have combined all three block fixes into a single class
|
||||
*
|
||||
*/
|
||||
public final class InvalidNameRegistrationBlocks {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(InvalidNameRegistrationBlocks.class);
|
||||
|
||||
public static Map<Integer, String> invalidBlocksNamesMap = new HashMap<Integer, String>()
|
||||
{
|
||||
{
|
||||
put(535658, "Qplay");
|
||||
put(536140, "Qweb");
|
||||
put(541334, "Qithub");
|
||||
}
|
||||
};
|
||||
|
||||
private InvalidNameRegistrationBlocks() {
|
||||
/* Do not instantiate */
|
||||
}
|
||||
|
||||
public static boolean isAffectedBlock(int height) {
|
||||
return (invalidBlocksNamesMap.containsKey(height));
|
||||
}
|
||||
|
||||
public static void processFix(Block block) throws DataException {
|
||||
Integer blockHeight = block.getBlockData().getHeight();
|
||||
String invalidName = invalidBlocksNamesMap.get(blockHeight);
|
||||
if (invalidName == null) {
|
||||
throw new DataException(String.format("Unable to lookup invalid name for block height %d", blockHeight));
|
||||
}
|
||||
|
||||
// Unregister the existing name record if it exists
|
||||
// This ensures that the duplicate name is considered valid, and therefore
|
||||
// the second (i.e. duplicate) REGISTER_NAME transaction data is applied.
|
||||
// Both were issued by the same user account, so there is no conflict.
|
||||
Name name = new Name(block.repository, invalidName);
|
||||
name.unregister();
|
||||
|
||||
LOGGER.debug("Applied name registration patch for block {}", blockHeight);
|
||||
}
|
||||
|
||||
// Note:
|
||||
// There is no need to write an orphanFix() method, as we do not have
|
||||
// the necessary ATStatesData to orphan back this far anyway
|
||||
|
||||
}
|
@@ -44,6 +44,9 @@ public class BlockMinter extends Thread {
|
||||
private static Long lastLogTimestamp;
|
||||
private static Long logTimeout;
|
||||
|
||||
// Recovery
|
||||
public static final long INVALID_BLOCK_RECOVERY_TIMEOUT = 10 * 60 * 1000L; // ms
|
||||
|
||||
// Constructors
|
||||
|
||||
public BlockMinter() {
|
||||
@@ -144,9 +147,25 @@ public class BlockMinter extends Thread {
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
continue;
|
||||
|
||||
// If we are stuck on an invalid block, we should allow an alternative to be minted
|
||||
boolean recoverInvalidBlock = false;
|
||||
if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
|
||||
// We've had at least one invalid block
|
||||
long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
|
||||
long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
|
||||
if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
||||
if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
||||
// Last valid block was more than 10 mins ago, but we've had an invalid block since then
|
||||
// Assume that the chain has stalled because there is no alternative valid candidate
|
||||
// Enter recovery mode to allow alternative, valid candidates to be minted
|
||||
recoverInvalidBlock = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
||||
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
if (Controller.getInstance().getRecoveryMode() == false)
|
||||
if (Controller.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
||||
continue;
|
||||
|
||||
// There are enough peers with a recent block and our latest block is recent
|
||||
@@ -230,6 +249,8 @@ public class BlockMinter extends Thread {
|
||||
if (testBlock.isTimestampValid() != ValidationResult.OK)
|
||||
continue;
|
||||
|
||||
testBlock.preProcess();
|
||||
|
||||
// Is new block valid yet? (Before adding unconfirmed transactions)
|
||||
ValidationResult result = testBlock.isValid();
|
||||
if (result != ValidationResult.OK) {
|
||||
@@ -421,7 +442,8 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// Add to blockchain
|
||||
newBlock.process();
|
||||
LOGGER.info(String.format("Minted new test block: %d", newBlock.getBlockData().getHeight()));
|
||||
LOGGER.info(String.format("Minted new test block: %d sig: %.8s",
|
||||
newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
|
@@ -46,6 +46,7 @@ import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.controller.repository.PruneManager;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
@@ -83,20 +84,14 @@ import org.qortal.network.message.OnlineAccountsMessage;
|
||||
import org.qortal.network.message.SignaturesMessage;
|
||||
import org.qortal.network.message.TransactionMessage;
|
||||
import org.qortal.network.message.TransactionSignaturesMessage;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryFactory;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.ArbitraryTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ByteArray;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.Triple;
|
||||
import org.qortal.utils.*;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
@@ -414,6 +409,7 @@ public class Controller extends Thread {
|
||||
try {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getRepositoryUrl());
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
RepositoryManager.archive();
|
||||
RepositoryManager.prune();
|
||||
} catch (DataException e) {
|
||||
// If exception has no cause then repository is in use by some other process.
|
||||
@@ -428,6 +424,11 @@ public class Controller extends Thread {
|
||||
return; // Not System.exit() so that GUI can display error
|
||||
}
|
||||
|
||||
// Rebuild Names table and check database integrity
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildAllNames();
|
||||
namesDatabaseIntegrityCheck.runIntegrityCheck();
|
||||
|
||||
LOGGER.info("Validating blockchain");
|
||||
try {
|
||||
BlockChain.validate();
|
||||
@@ -1286,6 +1287,34 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have no block data, we should check the archive in case it's there
|
||||
if (blockData == null) {
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, repository);
|
||||
if (bytes != null) {
|
||||
CachedBlockMessage blockMessage = new CachedBlockMessage(bytes);
|
||||
blockMessage.setId(message.getId());
|
||||
|
||||
// This call also causes the other needed data to be pulled in from repository
|
||||
if (!peer.sendMessage(blockMessage)) {
|
||||
peer.disconnect("failed to send block");
|
||||
// Don't fall-through to caching because failure to send might be from failure to build message
|
||||
return;
|
||||
}
|
||||
|
||||
// If request is for a recent block, cache it
|
||||
if (getChainHeight() - blockData.getHeight() <= blockCacheSize) {
|
||||
this.stats.getBlockMessageStats.cacheFills.incrementAndGet();
|
||||
|
||||
this.blockMessageCache.put(new ByteArray(blockData.getSignature()), blockMessage);
|
||||
}
|
||||
|
||||
// Sent successfully from archive, so nothing more to do
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockData == null) {
|
||||
// We don't have this block
|
||||
this.stats.getBlockMessageStats.unknownBlocks.getAndIncrement();
|
||||
|
@@ -3,12 +3,9 @@ package org.qortal.controller;
|
||||
import java.math.BigInteger;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -62,6 +59,11 @@ public class Synchronizer {
|
||||
// Keep track of the size of the last re-org, so it can be logged
|
||||
private int lastReorgSize;
|
||||
|
||||
// Keep track of invalid blocks so that we don't keep trying to sync them
|
||||
private Map<String, Long> invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
|
||||
public Long timeValidBlockLastReceived = null;
|
||||
public Long timeInvalidBlockLastReceived = null;
|
||||
|
||||
private static Synchronizer instance;
|
||||
|
||||
public enum SynchronizationResult {
|
||||
@@ -337,6 +339,12 @@ public class Synchronizer {
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore this peer if it holds an invalid block
|
||||
if (this.containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
|
||||
LOGGER.debug("Ignoring peer %s because it holds an invalid block", peer);
|
||||
peers.remove(peer);
|
||||
}
|
||||
|
||||
// Reduce minChainLength if needed. If we don't have any blocks, this peer will be excluded from chain weight comparisons later in the process, so we shouldn't update minChainLength
|
||||
List <BlockSummaryData> peerBlockSummaries = peer.getCommonBlockData().getBlockSummariesAfterCommonBlock();
|
||||
if (peerBlockSummaries != null && peerBlockSummaries.size() > 0)
|
||||
@@ -480,6 +488,71 @@ public class Synchronizer {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Invalid block signature tracking */
|
||||
|
||||
private void addInvalidBlockSignature(byte[] signature) {
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add or update existing entry
|
||||
String sig58 = Base58.encode(signature);
|
||||
invalidBlockSignatures.put(sig58, now);
|
||||
}
|
||||
private void deleteOlderInvalidSignatures(Long now) {
|
||||
if (now == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete signatures with older timestamps
|
||||
Iterator it = invalidBlockSignatures.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry pair = (Map.Entry)it.next();
|
||||
Long lastSeen = (Long) pair.getValue();
|
||||
|
||||
// Remove signature if we haven't seen it for more than 1 hour
|
||||
if (now - lastSeen > 60 * 60 * 1000L) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
private boolean containsInvalidBlockSummary(List<BlockSummaryData> blockSummaries) {
|
||||
if (blockSummaries == null || invalidBlockSignatures == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loop through our known invalid blocks and check each one against supplied block summaries
|
||||
for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
|
||||
byte[] invalidSignature = Base58.decode(invalidSignature58);
|
||||
for (BlockSummaryData blockSummary : blockSummaries) {
|
||||
byte[] signature = blockSummary.getSignature();
|
||||
if (Arrays.equals(signature, invalidSignature)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private boolean containsInvalidBlockSignature(List<byte[]> blockSignatures) {
|
||||
if (blockSignatures == null || invalidBlockSignatures == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loop through our known invalid blocks and check each one against supplied block signatures
|
||||
for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
|
||||
byte[] invalidSignature = Base58.decode(invalidSignature58);
|
||||
for (byte[] signature : blockSignatures) {
|
||||
if (Arrays.equals(signature, invalidSignature)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to synchronize blockchain with peer.
|
||||
* <p>
|
||||
@@ -526,6 +599,15 @@ public class Synchronizer {
|
||||
// Reset last re-org size as we are starting a new sync round
|
||||
this.lastReorgSize = 0;
|
||||
|
||||
// Set the initial value of timeValidBlockLastReceived if it's null
|
||||
Long now = NTP.getTime();
|
||||
if (this.timeValidBlockLastReceived == null) {
|
||||
this.timeValidBlockLastReceived = now;
|
||||
}
|
||||
|
||||
// Delete invalid signatures with older timestamps
|
||||
this.deleteOlderInvalidSignatures(now);
|
||||
|
||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||
SynchronizationResult findCommonBlockResult = fetchSummariesFromCommonBlock(repository, peer, ourInitialHeight, force, peerBlockSummaries, true);
|
||||
if (findCommonBlockResult != SynchronizationResult.OK) {
|
||||
@@ -874,6 +956,12 @@ public class Synchronizer {
|
||||
break;
|
||||
}
|
||||
|
||||
// Catch a block with an invalid signature before orphaning, so that we retain our existing valid candidate
|
||||
if (this.containsInvalidBlockSignature(peerBlockSignatures)) {
|
||||
LOGGER.info(String.format("Peer %s sent invalid block signature: %.8s", peer, Base58.encode(latestPeerSignature)));
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
byte[] nextPeerSignature = peerBlockSignatures.get(0);
|
||||
int nextHeight = height + 1;
|
||||
|
||||
@@ -976,13 +1064,20 @@ public class Synchronizer {
|
||||
if (Controller.isStopping())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
newBlock.preProcess();
|
||||
|
||||
ValidationResult blockResult = newBlock.isValid();
|
||||
if (blockResult != ValidationResult.OK) {
|
||||
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
|
||||
newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getSignature()), blockResult.name()));
|
||||
this.addInvalidBlockSignature(newBlock.getSignature());
|
||||
this.timeInvalidBlockLastReceived = NTP.getTime();
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// Block is valid
|
||||
this.timeValidBlockLastReceived = NTP.getTime();
|
||||
|
||||
// Save transactions attached to this block
|
||||
for (Transaction transaction : newBlock.getTransactions()) {
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
@@ -1064,13 +1159,20 @@ public class Synchronizer {
|
||||
for (Transaction transaction : newBlock.getTransactions())
|
||||
transaction.setInitialApprovalStatus();
|
||||
|
||||
newBlock.preProcess();
|
||||
|
||||
ValidationResult blockResult = newBlock.isValid();
|
||||
if (blockResult != ValidationResult.OK) {
|
||||
LOGGER.info(String.format("Peer %s sent invalid block for height %d, sig %.8s: %s", peer,
|
||||
ourHeight, Base58.encode(latestPeerSignature), blockResult.name()));
|
||||
this.addInvalidBlockSignature(newBlock.getSignature());
|
||||
this.timeInvalidBlockLastReceived = NTP.getTime();
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// Block is valid
|
||||
this.timeValidBlockLastReceived = NTP.getTime();
|
||||
|
||||
// Save transactions attached to this block
|
||||
for (Transaction transaction : newBlock.getTransactions()) {
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
|
@@ -18,15 +18,24 @@ public class AtStatesPruner implements Runnable {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("AT States pruner");
|
||||
|
||||
boolean archiveMode = false;
|
||||
if (!Settings.getInstance().isPruningEnabled()) {
|
||||
return;
|
||||
// Pruning isn't enabled, but we might want to prune for the purposes of archiving
|
||||
if (!Settings.getInstance().isArchiveEnabled()) {
|
||||
// No pruning or archiving, so we must not prune anything
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// We're allowed to prune blocks that have already been archived
|
||||
archiveMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int pruneStartHeight = repository.getATRepository().getAtPruneHeight();
|
||||
|
||||
repository.discardChanges();
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
repository.saveChanges();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
@@ -43,7 +52,14 @@ public class AtStatesPruner implements Runnable {
|
||||
|
||||
// Prune AT states for all blocks up until our latest minus pruneBlockLimit
|
||||
final int ourLatestHeight = chainTip.getHeight();
|
||||
final int upperPrunableHeight = ourLatestHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
int upperPrunableHeight = ourLatestHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
|
||||
// In archive mode we are only allowed to trim blocks that have already been archived
|
||||
if (archiveMode) {
|
||||
upperPrunableHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1;
|
||||
|
||||
// TODO: validate that the actual archived data exists before pruning it?
|
||||
}
|
||||
|
||||
int upperBatchHeight = pruneStartHeight + Settings.getInstance().getAtStatesPruneBatchSize();
|
||||
int upperPruneHeight = Math.min(upperBatchHeight, upperPrunableHeight);
|
||||
|
@@ -21,8 +21,8 @@ public class AtStatesTrimmer implements Runnable {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
repository.discardChanges();
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
repository.saveChanges();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
@@ -0,0 +1,111 @@
|
||||
package org.qortal.controller.repository;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class BlockArchiver implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiver.class);
|
||||
|
||||
private static final long INITIAL_SLEEP_PERIOD = 0L; // TODO: 5 * 60 * 1000L + 1234L; // ms
|
||||
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Block archiver");
|
||||
|
||||
if (!Settings.getInstance().isArchiveEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int startHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight();
|
||||
|
||||
// Don't attempt to archive if we have no ATStatesHeightIndex, as it will be too slow
|
||||
boolean hasAtStatesHeightIndex = repository.getATRepository().hasAtStatesHeightIndex();
|
||||
if (!hasAtStatesHeightIndex) {
|
||||
LOGGER.info("Unable to start block archiver due to missing ATStatesHeightIndex. Bootstrapping is recommended.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't even start building until initial rush has ended
|
||||
Thread.sleep(INITIAL_SLEEP_PERIOD);
|
||||
|
||||
LOGGER.info("Starting block archiver...");
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
|
||||
Thread.sleep(Settings.getInstance().getArchiveInterval());
|
||||
|
||||
BlockData chainTip = Controller.getInstance().getChainTip();
|
||||
if (chainTip == null || NTP.getTime() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't attempt to archive if we're not synced yet
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null || chainTip.getTimestamp() < minLatestBlockTimestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Build cache of blocks
|
||||
try {
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(startHeight, maximumArchiveHeight, repository);
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
switch (result) {
|
||||
case OK:
|
||||
// Increment block archive height
|
||||
startHeight += writer.getWrittenCount();
|
||||
repository.getBlockArchiveRepository().setBlockArchiveHeight(startHeight);
|
||||
repository.saveChanges();
|
||||
break;
|
||||
|
||||
case STOPPING:
|
||||
return;
|
||||
|
||||
// We've reached the limit of the blocks we can archive
|
||||
// Sleep for a while to allow more to become available
|
||||
case NOT_ENOUGH_BLOCKS:
|
||||
// We didn't reach our file size target, so that must mean that we don't have enough blocks
|
||||
// yet or something went wrong. Sleep for a while and then try again.
|
||||
Thread.sleep(60 * 60 * 1000L); // 1 hour
|
||||
break;
|
||||
|
||||
case BLOCK_NOT_FOUND:
|
||||
// We tried to archive a block that didn't exist. This is a major failure and likely means
|
||||
// that a bootstrap or re-sync is needed. Try again every minute until then.
|
||||
LOGGER.info("Error: block not found when building archive. If this error persists, " +
|
||||
"a bootstrap or re-sync may be needed.");
|
||||
Thread.sleep( 60 * 1000L); // 1 minute
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (IOException | TransformationException e) {
|
||||
LOGGER.info("Caught exception when creating block cache", e);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Caught exception when creating block cache", e);
|
||||
} catch (InterruptedException e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -18,13 +18,29 @@ public class BlockPruner implements Runnable {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Block pruner");
|
||||
|
||||
boolean archiveMode = false;
|
||||
if (!Settings.getInstance().isPruningEnabled()) {
|
||||
return;
|
||||
// Pruning isn't enabled, but we might want to prune for the purposes of archiving
|
||||
if (!Settings.getInstance().isArchiveEnabled()) {
|
||||
// No pruning or archiving, so we must not prune anything
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// We're allowed to prune blocks that have already been archived
|
||||
archiveMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int pruneStartHeight = repository.getBlockRepository().getBlockPruneHeight();
|
||||
|
||||
// Don't attempt to prune if we have no ATStatesHeightIndex, as it will be too slow
|
||||
boolean hasAtStatesHeightIndex = repository.getATRepository().hasAtStatesHeightIndex();
|
||||
if (!hasAtStatesHeightIndex) {
|
||||
LOGGER.info("Unable to start block pruner due to missing ATStatesHeightIndex. Bootstrapping is recommended.");
|
||||
return;
|
||||
}
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
@@ -35,12 +51,24 @@ public class BlockPruner implements Runnable {
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing())
|
||||
if (Controller.getInstance().isSynchronizing()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't attempt to prune if we're not synced yet
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null || chainTip.getTimestamp() < minLatestBlockTimestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prune all blocks up until our latest minus pruneBlockLimit
|
||||
final int ourLatestHeight = chainTip.getHeight();
|
||||
final int upperPrunableHeight = ourLatestHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
int upperPrunableHeight = ourLatestHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
|
||||
// In archive mode we are only allowed to trim blocks that have already been archived
|
||||
if (archiveMode) {
|
||||
upperPrunableHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1;
|
||||
}
|
||||
|
||||
int upperBatchHeight = pruneStartHeight + Settings.getInstance().getBlockPruneBatchSize();
|
||||
int upperPruneHeight = Math.min(upperBatchHeight, upperPrunableHeight);
|
||||
|
@@ -0,0 +1,410 @@
|
||||
package org.qortal.controller.repository;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.naming.Name;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.Unicode;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class NamesDatabaseIntegrityCheck {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(NamesDatabaseIntegrityCheck.class);
|
||||
|
||||
private static final List<TransactionType> ALL_NAME_TX_TYPE = Arrays.asList(
|
||||
TransactionType.REGISTER_NAME,
|
||||
TransactionType.UPDATE_NAME,
|
||||
TransactionType.BUY_NAME,
|
||||
TransactionType.SELL_NAME
|
||||
);
|
||||
|
||||
private List<TransactionData> nameTransactions = new ArrayList<>();
|
||||
|
||||
public int rebuildName(String name, Repository repository) {
|
||||
int modificationCount = 0;
|
||||
try {
|
||||
List<TransactionData> transactions = this.fetchAllTransactionsInvolvingName(name, repository);
|
||||
if (transactions.isEmpty()) {
|
||||
// This name was never registered, so there's nothing to do
|
||||
return modificationCount;
|
||||
}
|
||||
|
||||
// Loop through each past transaction and re-apply it to the Names table
|
||||
for (TransactionData currentTransaction : transactions) {
|
||||
|
||||
// Process REGISTER_NAME transactions
|
||||
if (currentTransaction.getType() == TransactionType.REGISTER_NAME) {
|
||||
RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) currentTransaction;
|
||||
Name nameObj = new Name(repository, registerNameTransactionData);
|
||||
nameObj.register();
|
||||
modificationCount++;
|
||||
LOGGER.trace("Processed REGISTER_NAME transaction for name {}", name);
|
||||
}
|
||||
|
||||
// Process UPDATE_NAME transactions
|
||||
if (currentTransaction.getType() == TransactionType.UPDATE_NAME) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) currentTransaction;
|
||||
|
||||
if (Objects.equals(updateNameTransactionData.getNewName(), name) &&
|
||||
!Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName())) {
|
||||
// This renames an existing name, so we need to process that instead
|
||||
this.rebuildName(updateNameTransactionData.getName(), repository);
|
||||
}
|
||||
else {
|
||||
Name nameObj = new Name(repository, name);
|
||||
if (nameObj != null && nameObj.getNameData() != null) {
|
||||
nameObj.update(updateNameTransactionData);
|
||||
modificationCount++;
|
||||
LOGGER.trace("Processed UPDATE_NAME transaction for name {}", name);
|
||||
} else {
|
||||
// Something went wrong
|
||||
throw new DataException(String.format("Name data not found for name %s", updateNameTransactionData.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process SELL_NAME transactions
|
||||
if (currentTransaction.getType() == TransactionType.SELL_NAME) {
|
||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) currentTransaction;
|
||||
Name nameObj = new Name(repository, sellNameTransactionData.getName());
|
||||
if (nameObj != null && nameObj.getNameData() != null) {
|
||||
nameObj.sell(sellNameTransactionData);
|
||||
modificationCount++;
|
||||
LOGGER.trace("Processed SELL_NAME transaction for name {}", name);
|
||||
}
|
||||
else {
|
||||
// Something went wrong
|
||||
throw new DataException(String.format("Name data not found for name %s", sellNameTransactionData.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
// Process BUY_NAME transactions
|
||||
if (currentTransaction.getType() == TransactionType.BUY_NAME) {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
|
||||
Name nameObj = new Name(repository, buyNameTransactionData.getName());
|
||||
if (nameObj != null && nameObj.getNameData() != null) {
|
||||
nameObj.buy(buyNameTransactionData);
|
||||
modificationCount++;
|
||||
LOGGER.trace("Processed BUY_NAME transaction for name {}", name);
|
||||
}
|
||||
else {
|
||||
// Something went wrong
|
||||
throw new DataException(String.format("Name data not found for name %s", buyNameTransactionData.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to run integrity check for name {}: {}", name, e.getMessage());
|
||||
}
|
||||
|
||||
return modificationCount;
|
||||
}
|
||||
|
||||
public int rebuildAllNames() {
|
||||
int modificationCount = 0;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<String> names = this.fetchAllNames(repository);
|
||||
for (String name : names) {
|
||||
modificationCount += this.rebuildName(name, repository);
|
||||
}
|
||||
repository.saveChanges();
|
||||
}
|
||||
catch (DataException e) {
|
||||
LOGGER.info("Error when running integrity check for all names: {}", e.getMessage());
|
||||
}
|
||||
|
||||
//LOGGER.info("modificationCount: {}", modificationCount);
|
||||
return modificationCount;
|
||||
}
|
||||
|
||||
public void runIntegrityCheck() {
|
||||
boolean integrityCheckFailed = false;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Fetch all the (confirmed) REGISTER_NAME transactions
|
||||
List<RegisterNameTransactionData> registerNameTransactions = this.fetchRegisterNameTransactions();
|
||||
|
||||
// Loop through each REGISTER_NAME txn signature and request the full transaction data
|
||||
for (RegisterNameTransactionData registerNameTransactionData : registerNameTransactions) {
|
||||
String registeredName = registerNameTransactionData.getName();
|
||||
NameData nameData = repository.getNameRepository().fromName(registeredName);
|
||||
|
||||
// Check to see if this name has been updated or bought at any point
|
||||
TransactionData latestUpdate = this.fetchLatestModificationTransactionInvolvingName(registeredName, repository);
|
||||
if (latestUpdate == null) {
|
||||
// Name was never updated once registered
|
||||
// We expect this name to still be registered to this transaction's creator
|
||||
|
||||
if (nameData == null) {
|
||||
LOGGER.info("Error: registered name {} doesn't exist in Names table. Adding...", registeredName);
|
||||
integrityCheckFailed = true;
|
||||
}
|
||||
else {
|
||||
LOGGER.trace("Registered name {} is correctly registered", registeredName);
|
||||
}
|
||||
|
||||
// Check the owner is correct
|
||||
PublicKeyAccount creator = new PublicKeyAccount(repository, registerNameTransactionData.getCreatorPublicKey());
|
||||
if (!Objects.equals(creator.getAddress(), nameData.getOwner())) {
|
||||
LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
|
||||
registeredName, nameData.getOwner(), creator.getAddress());
|
||||
integrityCheckFailed = true;
|
||||
}
|
||||
else {
|
||||
LOGGER.trace("Registered name {} has the correct owner", registeredName);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Check if owner is correct after update
|
||||
|
||||
// Check for name updates
|
||||
if (latestUpdate.getType() == TransactionType.UPDATE_NAME) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) latestUpdate;
|
||||
PublicKeyAccount creator = new PublicKeyAccount(repository, updateNameTransactionData.getCreatorPublicKey());
|
||||
|
||||
// When this name is the "new name", we expect the current owner to match the txn creator
|
||||
if (Objects.equals(updateNameTransactionData.getNewName(), registeredName)) {
|
||||
if (!Objects.equals(creator.getAddress(), nameData.getOwner())) {
|
||||
LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
|
||||
registeredName, nameData.getOwner(), creator.getAddress());
|
||||
integrityCheckFailed = true;
|
||||
}
|
||||
else {
|
||||
LOGGER.trace("Registered name {} has the correct owner after being updated", registeredName);
|
||||
}
|
||||
}
|
||||
|
||||
// When this name is the old name, we expect the "new name"'s owner to match the txn creator
|
||||
// The old name will then be unregistered, or re-registered.
|
||||
// FUTURE: check database integrity for names that have been updated and then the original name re-registered
|
||||
else if (Objects.equals(updateNameTransactionData.getName(), registeredName)) {
|
||||
NameData newNameData = repository.getNameRepository().fromName(updateNameTransactionData.getNewName());
|
||||
if (!Objects.equals(creator.getAddress(), newNameData.getOwner())) {
|
||||
LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
|
||||
updateNameTransactionData.getNewName(), newNameData.getOwner(), creator.getAddress());
|
||||
integrityCheckFailed = true;
|
||||
}
|
||||
else {
|
||||
LOGGER.trace("Registered name {} has the correct owner after being updated", updateNameTransactionData.getNewName());
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
LOGGER.info("Unhandled update case for name {}", registeredName);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for name buys
|
||||
else if (latestUpdate.getType() == TransactionType.BUY_NAME) {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) latestUpdate;
|
||||
PublicKeyAccount creator = new PublicKeyAccount(repository, buyNameTransactionData.getCreatorPublicKey());
|
||||
if (!Objects.equals(creator.getAddress(), nameData.getOwner())) {
|
||||
LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
|
||||
registeredName, nameData.getOwner(), creator.getAddress());
|
||||
integrityCheckFailed = true;
|
||||
}
|
||||
else {
|
||||
LOGGER.trace("Registered name {} has the correct owner after being bought", registeredName);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for name sells
|
||||
else if (latestUpdate.getType() == TransactionType.SELL_NAME) {
|
||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) latestUpdate;
|
||||
PublicKeyAccount creator = new PublicKeyAccount(repository, sellNameTransactionData.getCreatorPublicKey());
|
||||
if (!Objects.equals(creator.getAddress(), nameData.getOwner())) {
|
||||
LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
|
||||
registeredName, nameData.getOwner(), creator.getAddress());
|
||||
integrityCheckFailed = true;
|
||||
}
|
||||
else {
|
||||
LOGGER.trace("Registered name {} has the correct owner after being listed for sale", registeredName);
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
LOGGER.info("Unhandled case for name {}", registeredName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to trim online accounts signatures: %s", e.getMessage()));
|
||||
integrityCheckFailed = true;
|
||||
}
|
||||
|
||||
if (integrityCheckFailed) {
|
||||
LOGGER.info("Registered names database integrity check failed. Bootstrapping is recommended.");
|
||||
} else {
|
||||
LOGGER.info("Registered names database integrity check passed.");
|
||||
}
|
||||
}
|
||||
|
||||
private List<RegisterNameTransactionData> fetchRegisterNameTransactions() {
|
||||
List<RegisterNameTransactionData> registerNameTransactions = new ArrayList<>();
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
if (transactionData.getType() == TransactionType.REGISTER_NAME) {
|
||||
RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData;
|
||||
registerNameTransactions.add(registerNameTransactionData);
|
||||
}
|
||||
}
|
||||
return registerNameTransactions;
|
||||
}
|
||||
|
||||
private List<UpdateNameTransactionData> fetchUpdateNameTransactions() {
|
||||
List<UpdateNameTransactionData> updateNameTransactions = new ArrayList<>();
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
if (transactionData.getType() == TransactionType.UPDATE_NAME) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
||||
updateNameTransactions.add(updateNameTransactionData);
|
||||
}
|
||||
}
|
||||
return updateNameTransactions;
|
||||
}
|
||||
|
||||
private List<SellNameTransactionData> fetchSellNameTransactions() {
|
||||
List<SellNameTransactionData> sellNameTransactions = new ArrayList<>();
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
if (transactionData.getType() == TransactionType.SELL_NAME) {
|
||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
|
||||
sellNameTransactions.add(sellNameTransactionData);
|
||||
}
|
||||
}
|
||||
return sellNameTransactions;
|
||||
}
|
||||
|
||||
private List<BuyNameTransactionData> fetchBuyNameTransactions() {
|
||||
List<BuyNameTransactionData> buyNameTransactions = new ArrayList<>();
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
if (transactionData.getType() == TransactionType.BUY_NAME) {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
|
||||
buyNameTransactions.add(buyNameTransactionData);
|
||||
}
|
||||
}
|
||||
return buyNameTransactions;
|
||||
}
|
||||
|
||||
private void fetchAllNameTransactions(Repository repository) throws DataException {
|
||||
List<TransactionData> nameTransactions = new ArrayList<>();
|
||||
|
||||
// Fetch all the confirmed REGISTER_NAME transaction signatures
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(
|
||||
null, null, null, ALL_NAME_TX_TYPE, null, null,
|
||||
ConfirmationStatus.CONFIRMED, null, null, false);
|
||||
|
||||
for (byte[] signature : signatures) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
nameTransactions.add(transactionData);
|
||||
}
|
||||
this.nameTransactions = nameTransactions;
|
||||
}
|
||||
|
||||
private List<TransactionData> fetchAllTransactionsInvolvingName(String name, Repository repository) throws DataException {
|
||||
List<TransactionData> transactions = new ArrayList<>();
|
||||
String reducedName = Unicode.sanitize(name);
|
||||
|
||||
// Fetch all the confirmed name-modification transactions
|
||||
if (this.nameTransactions.isEmpty()) {
|
||||
this.fetchAllNameTransactions(repository);
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
|
||||
if ((transactionData instanceof RegisterNameTransactionData)) {
|
||||
RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData;
|
||||
if (Objects.equals(registerNameTransactionData.getReducedName(), reducedName)) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof UpdateNameTransactionData)) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
||||
if (Objects.equals(updateNameTransactionData.getName(), name) ||
|
||||
Objects.equals(updateNameTransactionData.getReducedNewName(), reducedName)) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof BuyNameTransactionData)) {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
|
||||
if (Objects.equals(buyNameTransactionData.getName(), name)) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof SellNameTransactionData)) {
|
||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
|
||||
if (Objects.equals(sellNameTransactionData.getName(), name)) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
private TransactionData fetchLatestModificationTransactionInvolvingName(String registeredName, Repository repository) throws DataException {
|
||||
List<TransactionData> transactionsInvolvingName = this.fetchAllTransactionsInvolvingName(registeredName, repository);
|
||||
|
||||
// Get the latest update for this name (excluding REGISTER_NAME transactions)
|
||||
TransactionData latestUpdateToName = transactionsInvolvingName.stream()
|
||||
.filter(txn -> txn.getType() != TransactionType.REGISTER_NAME)
|
||||
.max(Comparator.comparing(TransactionData::getTimestamp))
|
||||
.orElse(null);
|
||||
|
||||
return latestUpdateToName;
|
||||
}
|
||||
|
||||
private List<String> fetchAllNames(Repository repository) throws DataException {
|
||||
List<String> names = new ArrayList<>();
|
||||
|
||||
// Fetch all the confirmed name transactions
|
||||
if (this.nameTransactions.isEmpty()) {
|
||||
this.fetchAllNameTransactions(repository);
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
|
||||
if ((transactionData instanceof RegisterNameTransactionData)) {
|
||||
RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData;
|
||||
if (!names.contains(registerNameTransactionData.getName())) {
|
||||
names.add(registerNameTransactionData.getName());
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof UpdateNameTransactionData)) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
||||
if (!names.contains(updateNameTransactionData.getName())) {
|
||||
names.add(updateNameTransactionData.getName());
|
||||
}
|
||||
if (!names.contains(updateNameTransactionData.getNewName())) {
|
||||
names.add(updateNameTransactionData.getNewName());
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof BuyNameTransactionData)) {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
|
||||
if (!names.contains(buyNameTransactionData.getName())) {
|
||||
names.add(buyNameTransactionData.getName());
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof SellNameTransactionData)) {
|
||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
|
||||
if (!names.contains(sellNameTransactionData.getName())) {
|
||||
names.add(sellNameTransactionData.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
}
|
@@ -35,29 +35,70 @@ public class PruneManager {
|
||||
public void start() {
|
||||
this.executorService = Executors.newCachedThreadPool(new DaemonThreadFactory());
|
||||
|
||||
// Don't allow both the pruner and the trimmer to run at the same time.
|
||||
// In pruning mode, we are already deleting far more than we would when trimming.
|
||||
// In non-pruning mode, we still need to trim to keep the non-essential data
|
||||
// out of the database. There isn't a case where both are needed at once.
|
||||
// If we ever do need to enable both at once, be very careful with the AT state
|
||||
// trimming, since both currently rely on having exclusive access to the
|
||||
// prepareForAtStateTrimming() method. For both trimming and pruning to take place
|
||||
// at once, we would need to synchronize this method in a way that both can't
|
||||
// call it at the same time, as otherwise active ATs would be pruned/trimmed when
|
||||
// they should have been kept.
|
||||
|
||||
if (Settings.getInstance().isPruningEnabled()) {
|
||||
// Pruning enabled - start the pruning processes
|
||||
this.executorService.execute(new AtStatesPruner());
|
||||
this.executorService.execute(new BlockPruner());
|
||||
if (Settings.getInstance().isPruningEnabled() &&
|
||||
!Settings.getInstance().isArchiveEnabled()) {
|
||||
// Top-only-sync
|
||||
this.startTopOnlySyncMode();
|
||||
}
|
||||
else if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Full node with block archive
|
||||
this.startFullNodeWithBlockArchive();
|
||||
}
|
||||
else {
|
||||
// Pruning disabled - use trimming instead
|
||||
this.executorService.execute(new AtStatesTrimmer());
|
||||
this.executorService.execute(new OnlineAccountsSignaturesTrimmer());
|
||||
// Full node with full SQL support
|
||||
this.startFullSQLNode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-only-sync
|
||||
* In this mode, we delete (prune) all blocks except
|
||||
* a small number of recent ones. There is no need for
|
||||
* trimming or archiving, because all relevant blocks
|
||||
* are deleted.
|
||||
*/
|
||||
private void startTopOnlySyncMode() {
|
||||
this.startPruning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Full node with block archive
|
||||
* In this mode we archive trimmed blocks, and then
|
||||
* prune archived blocks to keep the database small
|
||||
*/
|
||||
private void startFullNodeWithBlockArchive() {
|
||||
this.startTrimming();
|
||||
this.startArchiving();
|
||||
this.startPruning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Full node with full SQL support
|
||||
* In this mode we trim the database but don't prune
|
||||
* or archive any data, because we want to maintain
|
||||
* full SQL support of old blocks. This mode will not
|
||||
* be actively maintained but can be used by those who
|
||||
* need to perform SQL analysis on older blocks.
|
||||
*/
|
||||
private void startFullSQLNode() {
|
||||
this.startTrimming();
|
||||
}
|
||||
|
||||
|
||||
private void startPruning() {
|
||||
this.executorService.execute(new AtStatesPruner());
|
||||
this.executorService.execute(new BlockPruner());
|
||||
}
|
||||
|
||||
private void startTrimming() {
|
||||
this.executorService.execute(new AtStatesTrimmer());
|
||||
this.executorService.execute(new OnlineAccountsSignaturesTrimmer());
|
||||
}
|
||||
|
||||
private void startArchiving() {
|
||||
this.executorService.execute(new BlockArchiver());
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
this.executorService.shutdownNow();
|
||||
|
||||
|
@@ -360,6 +360,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case BOB_DONE:
|
||||
case ALICE_REFUNDED:
|
||||
case BOB_REFUNDED:
|
||||
case ALICE_REFUNDING_A:
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
@@ -353,6 +353,7 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case BOB_DONE:
|
||||
case ALICE_REFUNDED:
|
||||
case BOB_REFUNDED:
|
||||
case ALICE_REFUNDING_A:
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
@@ -364,6 +364,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case BOB_DONE:
|
||||
case ALICE_REFUNDED:
|
||||
case BOB_REFUNDED:
|
||||
case ALICE_REFUNDING_A:
|
||||
return true;
|
||||
|
||||
default:
|
||||
|
@@ -21,6 +21,8 @@ public class Litecoin extends Bitcoiny {
|
||||
|
||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(10000); // 0.0001 LTC per 1000 bytes
|
||||
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 LTC minimum order, to avoid dust errors
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long MAINNET_FEE = 1000L;
|
||||
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||
@@ -164,6 +166,11 @@ public class Litecoin extends Bitcoiny {
|
||||
return DEFAULT_FEE_PER_KB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns estimated LTC fee, in sats per 1000bytes, optionally for historic timestamp.
|
||||
*
|
||||
|
47
src/main/java/org/qortal/data/block/BlockArchiveData.java
Normal file
47
src/main/java/org/qortal/data/block/BlockArchiveData.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package org.qortal.data.block;
|
||||
|
||||
import org.qortal.block.Block;
|
||||
|
||||
public class BlockArchiveData {
|
||||
|
||||
// Properties
|
||||
private byte[] signature;
|
||||
private Integer height;
|
||||
private Long timestamp;
|
||||
private byte[] minterPublicKey;
|
||||
|
||||
// Constructors
|
||||
|
||||
public BlockArchiveData(byte[] signature, Integer height, long timestamp, byte[] minterPublicKey) {
|
||||
this.signature = signature;
|
||||
this.height = height;
|
||||
this.timestamp = timestamp;
|
||||
this.minterPublicKey = minterPublicKey;
|
||||
}
|
||||
|
||||
public BlockArchiveData(BlockData blockData) {
|
||||
this.signature = blockData.getSignature();
|
||||
this.height = blockData.getHeight();
|
||||
this.timestamp = blockData.getTimestamp();
|
||||
this.minterPublicKey = blockData.getMinterPublicKey();
|
||||
}
|
||||
|
||||
// Getters/setters
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public Integer getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public byte[] getMinterPublicKey() {
|
||||
return this.minterPublicKey;
|
||||
}
|
||||
|
||||
}
|
@@ -26,7 +26,7 @@ public class RegisterNameTransactionData extends TransactionData {
|
||||
@Schema(description = "requested name", example = "my-name")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "simple name-related info in JSON format", example = "{ \"age\": 30 }")
|
||||
@Schema(description = "simple name-related info in JSON or text format", example = "Registered Name on the Qortal Chain")
|
||||
private String data;
|
||||
|
||||
// For internal use
|
||||
|
@@ -26,7 +26,7 @@ public class UpdateNameTransactionData extends TransactionData {
|
||||
@Schema(description = "new name", example = "my-new-name")
|
||||
private String newName;
|
||||
|
||||
@Schema(description = "replacement simple name-related info in JSON format", example = "{ \"age\": 30 }")
|
||||
@Schema(description = "replacement simple name-related info in JSON or text format", example = "Registered Name on the Qortal Chain")
|
||||
private String newData;
|
||||
|
||||
// For internal use
|
||||
|
@@ -78,9 +78,10 @@ public class Name {
|
||||
// Set name's last-updated timestamp
|
||||
this.nameData.setUpdated(updateNameTransactionData.getTimestamp());
|
||||
|
||||
// Update name and data where appropriate
|
||||
// Update name, reduced name, and data where appropriate
|
||||
if (!updateNameTransactionData.getNewName().isEmpty()) {
|
||||
this.nameData.setName(updateNameTransactionData.getNewName());
|
||||
this.nameData.setReducedName(updateNameTransactionData.getReducedNewName());
|
||||
|
||||
// If we're changing the name, we need to delete old entry
|
||||
this.repository.getNameRepository().delete(updateNameTransactionData.getName());
|
||||
@@ -106,6 +107,9 @@ public class Name {
|
||||
// We can find previous 'name' from update transaction
|
||||
this.nameData.setName(updateNameTransactionData.getName());
|
||||
|
||||
// We can derive the previous 'reduced name' from the previous name
|
||||
this.nameData.setReducedName(Unicode.sanitize(updateNameTransactionData.getName()));
|
||||
|
||||
// We might need to hunt for previous data value
|
||||
if (!updateNameTransactionData.getNewData().isEmpty())
|
||||
this.nameData.setData(findPreviousData(nameReference));
|
||||
@@ -261,4 +265,8 @@ public class Name {
|
||||
return previousTransactionData.getTimestamp();
|
||||
}
|
||||
|
||||
public NameData getNameData() {
|
||||
return this.nameData;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ public class CachedBlockMessage extends Message {
|
||||
this.block = block;
|
||||
}
|
||||
|
||||
private CachedBlockMessage(byte[] cachedBytes) {
|
||||
public CachedBlockMessage(byte[] cachedBytes) {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = null;
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -113,7 +115,10 @@ public interface ATRepository {
|
||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException;
|
||||
|
||||
|
||||
/** Rebuild the latest AT states cache, necessary for AT state trimming/pruning. */
|
||||
/** Rebuild the latest AT states cache, necessary for AT state trimming/pruning.
|
||||
* <p>
|
||||
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
|
||||
*/
|
||||
public void rebuildLatestAtStates() throws DataException;
|
||||
|
||||
|
||||
@@ -143,6 +148,10 @@ public interface ATRepository {
|
||||
public int pruneAtStates(int minHeight, int maxHeight) throws DataException;
|
||||
|
||||
|
||||
/** Checks for the presence of the ATStatesHeightIndex in repository */
|
||||
public boolean hasAtStatesHeightIndex() throws DataException;
|
||||
|
||||
|
||||
/**
|
||||
* Save ATStateData into repository.
|
||||
* <p>
|
||||
|
268
src/main/java/org/qortal/repository/BlockArchiveReader.java
Normal file
268
src/main/java/org/qortal/repository/BlockArchiveReader.java
Normal file
@@ -0,0 +1,268 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockArchiveData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import static org.qortal.transform.Transformer.INT_LENGTH;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
public class BlockArchiveReader {
|
||||
|
||||
private static BlockArchiveReader instance;
|
||||
private Map<String, Triple<Integer, Integer, Integer>> fileListCache = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiveReader.class);
|
||||
|
||||
public BlockArchiveReader() {
|
||||
|
||||
}
|
||||
|
||||
public static synchronized BlockArchiveReader getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new BlockArchiveReader();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void fetchFileList() {
|
||||
Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath();
|
||||
File archiveDirFile = archivePath.toFile();
|
||||
String[] files = archiveDirFile.list();
|
||||
Map<String, Triple<Integer, Integer, Integer>> map = new HashMap<>();
|
||||
|
||||
if (files != null) {
|
||||
for (String file : files) {
|
||||
Path filePath = Paths.get(file);
|
||||
String filename = filePath.getFileName().toString();
|
||||
|
||||
// Parse the filename
|
||||
if (filename == null || !filename.contains("-") || !filename.contains(".")) {
|
||||
// Not a usable file
|
||||
continue;
|
||||
}
|
||||
// Remove the extension and split into two parts
|
||||
String[] parts = filename.substring(0, filename.lastIndexOf('.')).split("-");
|
||||
Integer startHeight = Integer.parseInt(parts[0]);
|
||||
Integer endHeight = Integer.parseInt(parts[1]);
|
||||
Integer range = endHeight - startHeight;
|
||||
map.put(filename, new Triple(startHeight, endHeight, range));
|
||||
}
|
||||
}
|
||||
this.fileListCache = map;
|
||||
}
|
||||
|
||||
public Triple<BlockData, List<TransactionData>, List<ATStateData>> fetchBlockAtHeight(int height) {
|
||||
if (this.fileListCache.isEmpty()) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
byte[] serializedBytes = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBytes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(serializedBytes);
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = null;
|
||||
try {
|
||||
blockInfo = BlockTransformer.fromByteBuffer(byteBuffer);
|
||||
if (blockInfo != null && blockInfo.getA() != null) {
|
||||
// Block height is stored outside of the main serialized bytes, so it
|
||||
// won't be set automatically.
|
||||
blockInfo.getA().setHeight(height);
|
||||
}
|
||||
} catch (TransformationException e) {
|
||||
return null;
|
||||
}
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
public Triple<BlockData, List<TransactionData>, List<ATStateData>> fetchBlockWithSignature(
|
||||
byte[] signature, Repository repository) {
|
||||
|
||||
if (this.fileListCache.isEmpty()) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
Integer height = this.fetchHeightForSignature(signature, repository);
|
||||
if (height != null) {
|
||||
return this.fetchBlockAtHeight(height);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Triple<BlockData, List<TransactionData>, List<ATStateData>>> fetchBlocksFromRange(
|
||||
int startHeight, int endHeight) {
|
||||
|
||||
List<Triple<BlockData, List<TransactionData>, List<ATStateData>>> blockInfoList = new ArrayList<>();
|
||||
|
||||
for (int height = startHeight; height <= endHeight; height++) {
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = this.fetchBlockAtHeight(height);
|
||||
if (blockInfo == null) {
|
||||
return blockInfoList;
|
||||
}
|
||||
blockInfoList.add(blockInfo);
|
||||
}
|
||||
return blockInfoList;
|
||||
}
|
||||
|
||||
public Integer fetchHeightForSignature(byte[] signature, Repository repository) {
|
||||
// Lookup the height for the requested signature
|
||||
try {
|
||||
BlockArchiveData archivedBlock = repository.getBlockArchiveRepository().getBlockArchiveDataForSignature(signature);
|
||||
if (archivedBlock.getHeight() == null) {
|
||||
return null;
|
||||
}
|
||||
return archivedBlock.getHeight();
|
||||
|
||||
} catch (DataException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int fetchHeightForTimestamp(long timestamp, Repository repository) {
|
||||
// Lookup the height for the requested signature
|
||||
try {
|
||||
return repository.getBlockArchiveRepository().getHeightFromTimestamp(timestamp);
|
||||
|
||||
} catch (DataException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private String getFilenameForHeight(int height) {
|
||||
Iterator it = this.fileListCache.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry pair = (Map.Entry)it.next();
|
||||
if (pair == null && pair.getKey() == null && pair.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
Triple<Integer, Integer, Integer> heightInfo = (Triple<Integer, Integer, Integer>) pair.getValue();
|
||||
Integer startHeight = heightInfo.getA();
|
||||
Integer endHeight = heightInfo.getB();
|
||||
|
||||
if (height >= startHeight && height <= endHeight) {
|
||||
// Found the correct file
|
||||
String filename = (String) pair.getKey();
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] fetchSerializedBlockBytesForSignature(byte[] signature, Repository repository) {
|
||||
|
||||
if (this.fileListCache.isEmpty()) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
Integer height = this.fetchHeightForSignature(signature, repository);
|
||||
if (height != null) {
|
||||
return this.fetchSerializedBlockBytesForHeight(height);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] fetchSerializedBlockBytesForHeight(int height) {
|
||||
String filename = this.getFilenameForHeight(height);
|
||||
if (filename == null) {
|
||||
// We don't have this block in the archive
|
||||
// Invalidate the file list cache in case it is out of date
|
||||
this.invalidateFileListCache();
|
||||
return null;
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive", filename).toAbsolutePath();
|
||||
RandomAccessFile file = null;
|
||||
try {
|
||||
file = new RandomAccessFile(filePath.toString(), "r");
|
||||
// Get info about this file (the "fixed length header")
|
||||
final int version = file.readInt(); // Do not remove or comment out, as it is moving the file pointer
|
||||
final int startHeight = file.readInt(); // Do not remove or comment out, as it is moving the file pointer
|
||||
final int endHeight = file.readInt(); // Do not remove or comment out, as it is moving the file pointer
|
||||
file.readInt(); // Block count (unused) // Do not remove or comment out, as it is moving the file pointer
|
||||
final int variableHeaderLength = file.readInt(); // Do not remove or comment out, as it is moving the file pointer
|
||||
final int fixedHeaderLength = (int)file.getFilePointer();
|
||||
// End of fixed length header
|
||||
|
||||
// Make sure the version is one we recognize
|
||||
if (version != 1) {
|
||||
LOGGER.info("Error: unknown version in file {}: {}", filename, version);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify that the block is within the reported range
|
||||
if (height < startHeight || height > endHeight) {
|
||||
LOGGER.info("Error: requested height {} but the range of file {} is {}-{}",
|
||||
height, filename, startHeight, endHeight);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Seek to the location of the block index in the variable length header
|
||||
final int locationOfBlockIndexInVariableHeaderSegment = (height - startHeight) * INT_LENGTH;
|
||||
file.seek(fixedHeaderLength + locationOfBlockIndexInVariableHeaderSegment);
|
||||
|
||||
// Read the value to obtain the index of this block in the data segment
|
||||
int locationOfBlockInDataSegment = file.readInt();
|
||||
|
||||
// Now seek to the block data itself
|
||||
int dataSegmentStartIndex = fixedHeaderLength + variableHeaderLength + INT_LENGTH; // Confirmed correct
|
||||
file.seek(dataSegmentStartIndex + locationOfBlockInDataSegment);
|
||||
|
||||
// Read the block metadata
|
||||
int blockHeight = file.readInt(); // Do not remove or comment out, as it is moving the file pointer
|
||||
int blockLength = file.readInt(); // Do not remove or comment out, as it is moving the file pointer
|
||||
|
||||
// Ensure the block height matches the one requested
|
||||
if (blockHeight != height) {
|
||||
LOGGER.info("Error: height {} does not match requested: {}", blockHeight, height);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now retrieve the block's serialized bytes
|
||||
byte[] blockBytes = new byte[blockLength];
|
||||
file.read(blockBytes);
|
||||
|
||||
return blockBytes;
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
LOGGER.info("File {} not found: {}", filename, e.getMessage());
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to read block {} from archive: {}", height, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
finally {
|
||||
// Close the file
|
||||
if (file != null) {
|
||||
try {
|
||||
file.close();
|
||||
} catch (IOException e) {
|
||||
// Failed to close, but no need to handle this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidateFileListCache() {
|
||||
this.fileListCache.clear();
|
||||
}
|
||||
|
||||
}
|
130
src/main/java/org/qortal/repository/BlockArchiveRepository.java
Normal file
130
src/main/java/org/qortal/repository/BlockArchiveRepository.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.data.block.BlockArchiveData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BlockArchiveRepository {
|
||||
|
||||
/**
|
||||
* Returns BlockData from archive using block signature.
|
||||
*
|
||||
* @param signature
|
||||
* @return block data, or null if not found in archive.
|
||||
* @throws DataException
|
||||
*/
|
||||
public BlockData fromSignature(byte[] signature) throws DataException;
|
||||
|
||||
/**
|
||||
* Return height of block in archive using block's signature.
|
||||
*
|
||||
* @param signature
|
||||
* @return height, or 0 if not found in blockchain.
|
||||
* @throws DataException
|
||||
*/
|
||||
public int getHeightFromSignature(byte[] signature) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns BlockData from archive using block height.
|
||||
*
|
||||
* @param height
|
||||
* @return block data, or null if not found in blockchain.
|
||||
* @throws DataException
|
||||
*/
|
||||
public BlockData fromHeight(int height) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns a list of BlockData objects from archive using
|
||||
* block height range.
|
||||
*
|
||||
* @param startHeight
|
||||
* @return a list of BlockData objects, or an empty list if
|
||||
* not found in blockchain. It is not guaranteed that all
|
||||
* requested blocks will be returned.
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<BlockData> fromRange(int startHeight, int endHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns BlockData from archive using block reference.
|
||||
* Currently relies on a child block being the one block
|
||||
* higher than its parent. This limitation can be removed
|
||||
* by storing the reference in the BlockArchive table, but
|
||||
* this has been avoided to reduce space.
|
||||
*
|
||||
* @param reference
|
||||
* @return block data, or null if either parent or child
|
||||
* not found in the archive.
|
||||
* @throws DataException
|
||||
*/
|
||||
public BlockData fromReference(byte[] reference) throws DataException;
|
||||
|
||||
/**
|
||||
* Return height of block with timestamp just before passed timestamp.
|
||||
*
|
||||
* @param timestamp
|
||||
* @return height, or 0 if not found in blockchain.
|
||||
* @throws DataException
|
||||
*/
|
||||
public int getHeightFromTimestamp(long timestamp) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block summaries for blocks signed by passed public key, or reward-share with minter with passed public key.
|
||||
*/
|
||||
public List<BlockSummaryData> getBlockSummariesBySigner(byte[] signerPublicKey, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns summaries of block signers, optionally limited to passed addresses.
|
||||
* This combines both the BlockArchive and the Blocks data into a single result set.
|
||||
*/
|
||||
public List<BlockSignerSummary> getBlockSigners(List<String> addresses, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
|
||||
/** Returns height of first unarchived block. */
|
||||
public int getBlockArchiveHeight() throws DataException;
|
||||
|
||||
/** Sets new height for block archiving.
|
||||
* <p>
|
||||
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
|
||||
*/
|
||||
public void setBlockArchiveHeight(int archiveHeight) throws DataException;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the block archive data for a given signature, from the block archive.
|
||||
* <p>
|
||||
* This method will return null if no block archive has been built for the
|
||||
* requested signature. In those cases, the height (and other data) can be
|
||||
* looked up using the Blocks table. This allows a block to be located in
|
||||
* the archive when we only know its signature.
|
||||
* <p>
|
||||
*
|
||||
* @param signature
|
||||
* @throws DataException
|
||||
*/
|
||||
public BlockArchiveData getBlockArchiveDataForSignature(byte[] signature) throws DataException;
|
||||
|
||||
/**
|
||||
* Saves a block archive entry into the repository.
|
||||
* <p>
|
||||
* This can be used to find the height of a block by its signature, without
|
||||
* having access to the block data itself.
|
||||
* <p>
|
||||
*
|
||||
* @param blockArchiveData
|
||||
* @throws DataException
|
||||
*/
|
||||
public void save(BlockArchiveData blockArchiveData) throws DataException;
|
||||
|
||||
/**
|
||||
* Deletes a block archive entry from the repository.
|
||||
*
|
||||
* @param blockArchiveData
|
||||
* @throws DataException
|
||||
*/
|
||||
public void delete(BlockArchiveData blockArchiveData) throws DataException;
|
||||
|
||||
}
|
193
src/main/java/org/qortal/repository/BlockArchiveWriter.java
Normal file
193
src/main/java/org/qortal/repository/BlockArchiveWriter.java
Normal file
@@ -0,0 +1,193 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.block.BlockArchiveData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class BlockArchiveWriter {
|
||||
|
||||
public enum BlockArchiveWriteResult {
|
||||
OK,
|
||||
STOPPING,
|
||||
NOT_ENOUGH_BLOCKS,
|
||||
BLOCK_NOT_FOUND
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiveWriter.class);
|
||||
|
||||
private int startHeight;
|
||||
private final int endHeight;
|
||||
private final Repository repository;
|
||||
|
||||
private long fileSizeTarget = 100 * 1024 * 1024; // 100MiB
|
||||
private boolean shouldEnforceFileSizeTarget = true;
|
||||
|
||||
private int writtenCount;
|
||||
private Path outputPath;
|
||||
|
||||
public BlockArchiveWriter(int startHeight, int endHeight, Repository repository) {
|
||||
this.startHeight = startHeight;
|
||||
this.endHeight = endHeight;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public static int getMaxArchiveHeight(Repository repository) throws DataException {
|
||||
// We must only archive trimmed blocks, or the archive will grow far too large
|
||||
final int accountSignaturesTrimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
final int atTrimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
final int trimStartHeight = Math.min(accountSignaturesTrimStartHeight, atTrimStartHeight);
|
||||
return trimStartHeight - 1; // subtract 1 because these values represent the first _untrimmed_ block
|
||||
}
|
||||
|
||||
public static boolean isArchiverUpToDate(Repository repository) throws DataException {
|
||||
final int maxArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
final int actualArchiveHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight();
|
||||
final float progress = (float)actualArchiveHeight / (float) maxArchiveHeight;
|
||||
LOGGER.info(String.format("maxArchiveHeight: %d, actualArchiveHeight: %d, progress: %f",
|
||||
maxArchiveHeight, actualArchiveHeight, progress));
|
||||
|
||||
// If archiver is within 90% of the maximum, treat it as up to date
|
||||
// We need several percent as an allowance because the archiver will only
|
||||
// save files when they reach the target size
|
||||
return (progress >= 0.90);
|
||||
}
|
||||
|
||||
public BlockArchiveWriteResult write() throws DataException, IOException, TransformationException, InterruptedException {
|
||||
// Create the archive folder if it doesn't exist
|
||||
// This is a subfolder of the db directory, to make bootstrapping easier
|
||||
Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath();
|
||||
try {
|
||||
Files.createDirectories(archivePath);
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to create archive folder");
|
||||
throw new DataException("Unable to create archive folder");
|
||||
}
|
||||
|
||||
// Determine start height of blocks to fetch
|
||||
if (startHeight <= 2) {
|
||||
// Skip genesis block, as it's not designed to be transmitted, and we can build that from blockchain.json
|
||||
// TODO: include genesis block if we can
|
||||
startHeight = 2;
|
||||
}
|
||||
|
||||
// Header bytes will store the block indexes
|
||||
ByteArrayOutputStream headerBytes = new ByteArrayOutputStream();
|
||||
// Bytes will store the actual block data
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
LOGGER.info(String.format("Fetching blocks from height %d...", startHeight));
|
||||
int i = 0;
|
||||
while (headerBytes.size() + bytes.size() < this.fileSizeTarget
|
||||
|| this.shouldEnforceFileSizeTarget == false) {
|
||||
|
||||
if (Controller.isStopping()) {
|
||||
return BlockArchiveWriteResult.STOPPING;
|
||||
}
|
||||
if (Controller.getInstance().isSynchronizing()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int currentHeight = startHeight + i;
|
||||
if (currentHeight > endHeight) {
|
||||
break;
|
||||
}
|
||||
|
||||
//LOGGER.info("Fetching block {}...", currentHeight);
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(currentHeight);
|
||||
if (blockData == null) {
|
||||
return BlockArchiveWriteResult.BLOCK_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Write the signature and height into the BlockArchive table
|
||||
BlockArchiveData blockArchiveData = new BlockArchiveData(blockData);
|
||||
repository.getBlockArchiveRepository().save(blockArchiveData);
|
||||
repository.saveChanges();
|
||||
|
||||
// Write the block data to some byte buffers
|
||||
Block block = new Block(repository, blockData);
|
||||
int blockIndex = bytes.size();
|
||||
// Write block index to header
|
||||
headerBytes.write(Ints.toByteArray(blockIndex));
|
||||
// Write block height
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
byte[] blockBytes = BlockTransformer.toBytes(block);
|
||||
// Write block length
|
||||
bytes.write(Ints.toByteArray(blockBytes.length));
|
||||
// Write block bytes
|
||||
bytes.write(blockBytes);
|
||||
i++;
|
||||
|
||||
}
|
||||
int totalLength = headerBytes.size() + bytes.size();
|
||||
LOGGER.info(String.format("Total length of %d blocks is %d bytes", i, totalLength));
|
||||
|
||||
// Validate file size, in case something went wrong
|
||||
if (totalLength < fileSizeTarget && this.shouldEnforceFileSizeTarget) {
|
||||
return BlockArchiveWriteResult.NOT_ENOUGH_BLOCKS;
|
||||
}
|
||||
|
||||
// We have enough blocks to create a new file
|
||||
int endHeight = startHeight + i - 1;
|
||||
int version = 1;
|
||||
String filePath = String.format("%s/%d-%d.dat", archivePath.toString(), startHeight, endHeight);
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
|
||||
// Write version number
|
||||
fileOutputStream.write(Ints.toByteArray(version));
|
||||
// Write start height
|
||||
fileOutputStream.write(Ints.toByteArray(startHeight));
|
||||
// Write end height
|
||||
fileOutputStream.write(Ints.toByteArray(endHeight));
|
||||
// Write total count
|
||||
fileOutputStream.write(Ints.toByteArray(i));
|
||||
// Write dynamic header (block indexes) segment length
|
||||
fileOutputStream.write(Ints.toByteArray(headerBytes.size()));
|
||||
// Write dynamic header (block indexes) data
|
||||
headerBytes.writeTo(fileOutputStream);
|
||||
// Write data segment (block data) length
|
||||
fileOutputStream.write(Ints.toByteArray(bytes.size()));
|
||||
// Write data
|
||||
bytes.writeTo(fileOutputStream);
|
||||
// Close the file
|
||||
fileOutputStream.close();
|
||||
|
||||
// Invalidate cache so that the rest of the app picks up the new file
|
||||
BlockArchiveReader.getInstance().invalidateFileListCache();
|
||||
|
||||
this.writtenCount = i;
|
||||
this.outputPath = Paths.get(filePath);
|
||||
return BlockArchiveWriteResult.OK;
|
||||
}
|
||||
|
||||
public int getWrittenCount() {
|
||||
return this.writtenCount;
|
||||
}
|
||||
|
||||
public Path getOutputPath() {
|
||||
return this.outputPath;
|
||||
}
|
||||
|
||||
public void setFileSizeTarget(long fileSizeTarget) {
|
||||
this.fileSizeTarget = fileSizeTarget;
|
||||
}
|
||||
|
||||
// For testing, to avoid having to pre-calculate file sizes
|
||||
public void setShouldEnforceFileSizeTarget(boolean shouldEnforceFileSizeTarget) {
|
||||
this.shouldEnforceFileSizeTarget = shouldEnforceFileSizeTarget;
|
||||
}
|
||||
|
||||
}
|
@@ -137,11 +137,6 @@ public interface BlockRepository {
|
||||
*/
|
||||
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException;
|
||||
|
||||
/**
|
||||
* Returns block summaries for the passed height range, for API use.
|
||||
*/
|
||||
public List<BlockSummaryData> getBlockSummaries(Integer startHeight, Integer endHeight, Integer count) throws DataException;
|
||||
|
||||
/** Returns height of first trimmable online accounts signatures. */
|
||||
public int getOnlineAccountsSignaturesTrimHeight() throws DataException;
|
||||
|
||||
|
@@ -12,6 +12,8 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public BlockRepository getBlockRepository();
|
||||
|
||||
public BlockArchiveRepository getBlockArchiveRepository();
|
||||
|
||||
public ChatRepository getChatRepository();
|
||||
|
||||
public CrossChainRepository getCrossChainRepository();
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.repository;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.repository.hsqldb.HSQLDBDatabaseArchiving;
|
||||
import org.qortal.repository.hsqldb.HSQLDBDatabasePruning;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
@@ -57,22 +58,48 @@ public abstract class RepositoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static void prune() {
|
||||
// Bulk prune the database the first time we use pruning mode
|
||||
if (Settings.getInstance().isPruningEnabled()) {
|
||||
try {
|
||||
boolean prunedATStates = HSQLDBDatabasePruning.pruneATStates();
|
||||
boolean prunedBlocks = HSQLDBDatabasePruning.pruneBlocks();
|
||||
public static boolean archive() {
|
||||
// Bulk archive the database the first time we use archive mode
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
if (RepositoryManager.canArchiveOrPrune()) {
|
||||
try {
|
||||
return HSQLDBDatabaseArchiving.buildBlockArchive();
|
||||
|
||||
// Perform repository maintenance to shrink the db size down
|
||||
if (prunedATStates && prunedBlocks) {
|
||||
HSQLDBDatabasePruning.performMaintenance();
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to build block archive. The database may have been left in an inconsistent state.");
|
||||
}
|
||||
|
||||
} catch (SQLException | DataException e) {
|
||||
LOGGER.info("Unable to bulk prune AT states. The database may have been left in an inconsistent state.");
|
||||
}
|
||||
else {
|
||||
LOGGER.info("Unable to build block archive due to missing ATStatesHeightIndex. Bootstrapping is recommended.");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean prune() {
|
||||
// Bulk prune the database the first time we use pruning mode
|
||||
if (Settings.getInstance().isPruningEnabled() ||
|
||||
Settings.getInstance().isArchiveEnabled()) {
|
||||
if (RepositoryManager.canArchiveOrPrune()) {
|
||||
try {
|
||||
boolean prunedATStates = HSQLDBDatabasePruning.pruneATStates();
|
||||
boolean prunedBlocks = HSQLDBDatabasePruning.pruneBlocks();
|
||||
|
||||
// Perform repository maintenance to shrink the db size down
|
||||
if (prunedATStates && prunedBlocks) {
|
||||
HSQLDBDatabasePruning.performMaintenance();
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (SQLException | DataException e) {
|
||||
LOGGER.info("Unable to bulk prune AT states. The database may have been left in an inconsistent state.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.info("Unable to prune blocks due to missing ATStatesHeightIndex. Bootstrapping is recommended.");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void setRequestedCheckpoint(Boolean quick) {
|
||||
@@ -101,4 +128,12 @@ public abstract class RepositoryManager {
|
||||
return SQLException.class.isInstance(cause) && repositoryFactory.isDeadlockException((SQLException) cause);
|
||||
}
|
||||
|
||||
public static boolean canArchiveOrPrune() {
|
||||
try (final Repository repository = getRepository()) {
|
||||
return repository.getATRepository().hasAtStatesHeightIndex();
|
||||
} catch (DataException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -604,28 +604,37 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public void rebuildLatestAtStates() throws DataException {
|
||||
// Rebuild cache of latest AT states that we can't trim
|
||||
String deleteSql = "DELETE FROM LatestATStates";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(deleteSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to delete temporary latest AT states cache from repository", e);
|
||||
}
|
||||
// latestATStatesLock is to prevent concurrent updates on LatestATStates
|
||||
// that could result in one process using a partial or empty dataset
|
||||
// because it was in the process of being rebuilt by another thread
|
||||
synchronized (this.repository.latestATStatesLock) {
|
||||
LOGGER.trace("Rebuilding latest AT states...");
|
||||
|
||||
String insertSql = "INSERT INTO LatestATStates ("
|
||||
+ "SELECT AT_address, height FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
|
||||
+ ") "
|
||||
+ ")";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(insertSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);
|
||||
// Rebuild cache of latest AT states that we can't trim
|
||||
String deleteSql = "DELETE FROM LatestATStates";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(deleteSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to delete temporary latest AT states cache from repository", e);
|
||||
}
|
||||
|
||||
String insertSql = "INSERT INTO LatestATStates ("
|
||||
+ "SELECT AT_address, height FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
|
||||
+ ") "
|
||||
+ ")";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(insertSql);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);
|
||||
}
|
||||
this.repository.saveChanges();
|
||||
LOGGER.trace("Rebuilt latest AT states");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,7 +664,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
this.repository.executeCheckedUpdate(updateSql, trimHeight);
|
||||
this.repository.saveChanges();
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
this.repository.examineException(e);
|
||||
throw new DataException("Unable to set AT state trim height in repository", e);
|
||||
}
|
||||
}
|
||||
@@ -666,22 +675,31 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
if (minHeight >= maxHeight)
|
||||
return 0;
|
||||
|
||||
// We're often called so no need to trim all states in one go.
|
||||
// Limit updates to reduce CPU and memory load.
|
||||
String sql = "DELETE FROM ATStatesData "
|
||||
+ "WHERE height BETWEEN ? AND ? "
|
||||
+ "AND NOT EXISTS("
|
||||
// latestATStatesLock is to prevent concurrent updates on LatestATStates
|
||||
// that could result in one process using a partial or empty dataset
|
||||
// because it was in the process of being rebuilt by another thread
|
||||
synchronized (this.repository.latestATStatesLock) {
|
||||
|
||||
// We're often called so no need to trim all states in one go.
|
||||
// Limit updates to reduce CPU and memory load.
|
||||
String sql = "DELETE FROM ATStatesData "
|
||||
+ "WHERE height BETWEEN ? AND ? "
|
||||
+ "AND NOT EXISTS("
|
||||
+ "SELECT TRUE FROM LatestATStates "
|
||||
+ "WHERE LatestATStates.AT_address = ATStatesData.AT_address "
|
||||
+ "AND LatestATStates.height = ATStatesData.height"
|
||||
+ ") "
|
||||
+ "LIMIT ?";
|
||||
+ ") "
|
||||
+ "LIMIT ?";
|
||||
|
||||
try {
|
||||
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight, limit);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to trim AT states in repository", e);
|
||||
try {
|
||||
int modifiedRows = this.repository.executeCheckedUpdate(sql, minHeight, maxHeight, limit);
|
||||
this.repository.saveChanges();
|
||||
return modifiedRows;
|
||||
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to trim AT states in repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,57 +737,77 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public int pruneAtStates(int minHeight, int maxHeight) throws DataException {
|
||||
int deletedCount = 0;
|
||||
// latestATStatesLock is to prevent concurrent updates on LatestATStates
|
||||
// that could result in one process using a partial or empty dataset
|
||||
// because it was in the process of being rebuilt by another thread
|
||||
synchronized (this.repository.latestATStatesLock) {
|
||||
|
||||
for (int height=minHeight; height<maxHeight; height++) {
|
||||
int deletedCount = 0;
|
||||
|
||||
// Give up if we're stopping
|
||||
if (Controller.isStopping()) {
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
// Get latest AT states for this height
|
||||
List<String> atAddresses = new ArrayList<>();
|
||||
String updateSql = "SELECT AT_address FROM LatestATStates WHERE height = ?";
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(updateSql, height)) {
|
||||
if (resultSet != null) {
|
||||
do {
|
||||
String atAddress = resultSet.getString(1);
|
||||
atAddresses.add(atAddress);
|
||||
|
||||
} while (resultSet.next());
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch flagged accounts from repository", e);
|
||||
}
|
||||
|
||||
List<ATStateData> atStates = this.getBlockATStatesAtHeight(height);
|
||||
for (ATStateData atState : atStates) {
|
||||
//LOGGER.info("Found atState {} at height {}", atState.getATAddress(), atState.getHeight());
|
||||
for (int height = minHeight; height <= maxHeight; height++) {
|
||||
|
||||
// Give up if we're stopping
|
||||
if (Controller.isStopping()) {
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
if (atAddresses.contains(atState.getATAddress())) {
|
||||
// We don't want to delete this AT state because it is still active
|
||||
LOGGER.info("Skipping atState {} at height {}", atState.getATAddress(), atState.getHeight());
|
||||
continue;
|
||||
// Get latest AT states for this height
|
||||
List<String> atAddresses = new ArrayList<>();
|
||||
String updateSql = "SELECT AT_address FROM LatestATStates WHERE height = ?";
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(updateSql, height)) {
|
||||
if (resultSet != null) {
|
||||
do {
|
||||
String atAddress = resultSet.getString(1);
|
||||
atAddresses.add(atAddress);
|
||||
|
||||
} while (resultSet.next());
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch latest AT states from repository", e);
|
||||
}
|
||||
|
||||
// Safe to delete everything else for this height
|
||||
try {
|
||||
this.repository.delete("ATStates", "AT_address = ? AND height = ?",
|
||||
atState.getATAddress(), atState.getHeight());
|
||||
deletedCount++;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete AT state data from repository", e);
|
||||
List<ATStateData> atStates = this.getBlockATStatesAtHeight(height);
|
||||
for (ATStateData atState : atStates) {
|
||||
//LOGGER.info("Found atState {} at height {}", atState.getATAddress(), atState.getHeight());
|
||||
|
||||
// Give up if we're stopping
|
||||
if (Controller.isStopping()) {
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
if (atAddresses.contains(atState.getATAddress())) {
|
||||
// We don't want to delete this AT state because it is still active
|
||||
LOGGER.info("Skipping atState {} at height {}", atState.getATAddress(), atState.getHeight());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safe to delete everything else for this height
|
||||
try {
|
||||
this.repository.delete("ATStates", "AT_address = ? AND height = ?",
|
||||
atState.getATAddress(), atState.getHeight());
|
||||
deletedCount++;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete AT state data from repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.repository.saveChanges();
|
||||
|
||||
return deletedCount;
|
||||
return deletedCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasAtStatesHeightIndex() throws DataException {
|
||||
String sql = "SELECT INDEX_NAME FROM INFORMATION_SCHEMA.SYSTEM_INDEXINFO where INDEX_NAME='ATSTATESHEIGHTINDEX'";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
return resultSet != null;
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to check for ATStatesHeightIndex in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -0,0 +1,292 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.block.BlockArchiveData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.repository.BlockArchiveReader;
|
||||
import org.qortal.repository.BlockArchiveRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class HSQLDBBlockArchiveRepository implements BlockArchiveRepository {
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
|
||||
public HSQLDBBlockArchiveRepository(HSQLDBRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public BlockData fromSignature(byte[] signature) throws DataException {
|
||||
Triple blockInfo = BlockArchiveReader.getInstance().fetchBlockWithSignature(signature, this.repository);
|
||||
if (blockInfo != null) {
|
||||
return (BlockData) blockInfo.getA();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeightFromSignature(byte[] signature) throws DataException {
|
||||
Integer height = BlockArchiveReader.getInstance().fetchHeightForSignature(signature, this.repository);
|
||||
if (height == null || height == 0) {
|
||||
return 0;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData fromHeight(int height) throws DataException {
|
||||
Triple blockInfo = BlockArchiveReader.getInstance().fetchBlockAtHeight(height);
|
||||
if (blockInfo != null) {
|
||||
return (BlockData) blockInfo.getA();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockData> fromRange(int startHeight, int endHeight) throws DataException {
|
||||
List<BlockData> blocks = new ArrayList<>();
|
||||
|
||||
for (int height = startHeight; height < endHeight; height++) {
|
||||
BlockData blockData = this.fromHeight(height);
|
||||
if (blockData == null) {
|
||||
return blocks;
|
||||
}
|
||||
blocks.add(blockData);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData fromReference(byte[] reference) throws DataException {
|
||||
BlockData referenceBlock = this.repository.getBlockArchiveRepository().fromSignature(reference);
|
||||
if (referenceBlock != null) {
|
||||
int height = referenceBlock.getHeight();
|
||||
if (height > 0) {
|
||||
// Request the block at height + 1
|
||||
Triple blockInfo = BlockArchiveReader.getInstance().fetchBlockAtHeight(height + 1);
|
||||
if (blockInfo != null) {
|
||||
return (BlockData) blockInfo.getA();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeightFromTimestamp(long timestamp) throws DataException {
|
||||
String sql = "SELECT height FROM BlockArchive WHERE minted_when <= ? ORDER BY minted_when DESC, height DESC LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, timestamp)) {
|
||||
if (resultSet == null) {
|
||||
return 0;
|
||||
}
|
||||
return resultSet.getInt(1);
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Error fetching height from BlockArchive repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockSummaryData> getBlockSummariesBySigner(byte[] signerPublicKey, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
sql.append("SELECT signature, height, BlockArchive.minter FROM ");
|
||||
|
||||
// List of minter account's public key and reward-share public keys with minter's public key
|
||||
sql.append("(SELECT * FROM (VALUES (CAST(? AS QortalPublicKey))) UNION (SELECT reward_share_public_key FROM RewardShares WHERE minter_public_key = ?)) AS PublicKeys (public_key) ");
|
||||
|
||||
// Match BlockArchive blocks signed with public key from above list
|
||||
sql.append("JOIN BlockArchive ON BlockArchive.minter = public_key ");
|
||||
|
||||
sql.append("ORDER BY BlockArchive.height ");
|
||||
if (reverse != null && reverse)
|
||||
sql.append("DESC ");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), signerPublicKey, signerPublicKey)) {
|
||||
if (resultSet == null)
|
||||
return blockSummaries;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
int height = resultSet.getInt(2);
|
||||
byte[] blockMinterPublicKey = resultSet.getBytes(3);
|
||||
|
||||
// Fetch additional info from the archive itself
|
||||
int onlineAccountsCount = 0;
|
||||
BlockData blockData = this.fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
onlineAccountsCount = blockData.getOnlineAccountsCount();
|
||||
}
|
||||
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount);
|
||||
blockSummaries.add(blockSummary);
|
||||
} while (resultSet.next());
|
||||
|
||||
return blockSummaries;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch minter's block summaries from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockSignerSummary> getBlockSigners(List<String> addresses, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
String subquerySql = "SELECT minter, COUNT(signature) FROM (" +
|
||||
"(SELECT minter, signature FROM Blocks) UNION ALL (SELECT minter, signature FROM BlockArchive)" +
|
||||
") GROUP BY minter";
|
||||
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT DISTINCT block_minter, n_blocks, minter_public_key, minter, recipient FROM (");
|
||||
sql.append(subquerySql);
|
||||
sql.append(") AS Minters (block_minter, n_blocks) LEFT OUTER JOIN RewardShares ON reward_share_public_key = block_minter ");
|
||||
|
||||
if (addresses != null && !addresses.isEmpty()) {
|
||||
sql.append(" LEFT OUTER JOIN Accounts AS BlockMinterAccounts ON BlockMinterAccounts.public_key = block_minter ");
|
||||
sql.append(" LEFT OUTER JOIN Accounts AS RewardShareMinterAccounts ON RewardShareMinterAccounts.public_key = minter_public_key ");
|
||||
sql.append(" JOIN (VALUES ");
|
||||
|
||||
final int addressesSize = addresses.size();
|
||||
for (int ai = 0; ai < addressesSize; ++ai) {
|
||||
if (ai != 0)
|
||||
sql.append(", ");
|
||||
|
||||
sql.append("(?)");
|
||||
}
|
||||
|
||||
sql.append(") AS FilterAccounts (account) ");
|
||||
sql.append(" ON FilterAccounts.account IN (recipient, BlockMinterAccounts.account, RewardShareMinterAccounts.account) ");
|
||||
} else {
|
||||
addresses = Collections.emptyList();
|
||||
}
|
||||
|
||||
sql.append("ORDER BY n_blocks ");
|
||||
if (reverse != null && reverse)
|
||||
sql.append("DESC ");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<BlockSignerSummary> summaries = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), addresses.toArray())) {
|
||||
if (resultSet == null)
|
||||
return summaries;
|
||||
|
||||
do {
|
||||
byte[] blockMinterPublicKey = resultSet.getBytes(1);
|
||||
int nBlocks = resultSet.getInt(2);
|
||||
|
||||
// May not be present if no reward-share:
|
||||
byte[] mintingAccountPublicKey = resultSet.getBytes(3);
|
||||
String minterAccount = resultSet.getString(4);
|
||||
String recipientAccount = resultSet.getString(5);
|
||||
|
||||
BlockSignerSummary blockSignerSummary;
|
||||
if (recipientAccount == null)
|
||||
blockSignerSummary = new BlockSignerSummary(blockMinterPublicKey, nBlocks);
|
||||
else
|
||||
blockSignerSummary = new BlockSignerSummary(blockMinterPublicKey, nBlocks, mintingAccountPublicKey, minterAccount, recipientAccount);
|
||||
|
||||
summaries.add(blockSignerSummary);
|
||||
} while (resultSet.next());
|
||||
|
||||
return summaries;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch block minters from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getBlockArchiveHeight() throws DataException {
|
||||
String sql = "SELECT block_archive_height FROM DatabaseInfo";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return 0;
|
||||
|
||||
return resultSet.getInt(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch block archive height from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBlockArchiveHeight(int archiveHeight) throws DataException {
|
||||
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
|
||||
// that could result in "transaction rollback: serialization failure"
|
||||
synchronized (this.repository.trimHeightsLock) {
|
||||
String updateSql = "UPDATE DatabaseInfo SET block_archive_height = ?";
|
||||
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(updateSql, archiveHeight);
|
||||
this.repository.saveChanges();
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to set block archive height in repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public BlockArchiveData getBlockArchiveDataForSignature(byte[] signature) throws DataException {
|
||||
String sql = "SELECT height, signature, minted_when, minter FROM BlockArchive WHERE signature = ? LIMIT 1";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, signature)) {
|
||||
if (resultSet == null) {
|
||||
return null;
|
||||
}
|
||||
int height = resultSet.getInt(1);
|
||||
byte[] sig = resultSet.getBytes(2);
|
||||
long timestamp = resultSet.getLong(3);
|
||||
byte[] minterPublicKey = resultSet.getBytes(4);
|
||||
return new BlockArchiveData(sig, height, timestamp, minterPublicKey);
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Error fetching height from BlockArchive repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void save(BlockArchiveData blockArchiveData) throws DataException {
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("BlockArchive");
|
||||
|
||||
saveHelper.bind("signature", blockArchiveData.getSignature())
|
||||
.bind("height", blockArchiveData.getHeight())
|
||||
.bind("minted_when", blockArchiveData.getTimestamp())
|
||||
.bind("minter", blockArchiveData.getMinterPublicKey());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save SimpleBlockData into BlockArchive repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(BlockArchiveData blockArchiveData) throws DataException {
|
||||
try {
|
||||
this.repository.delete("BlockArchive",
|
||||
"block_signature = ?", blockArchiveData.getSignature());
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to delete SimpleBlockData from BlockArchive repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -10,6 +10,7 @@ import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.BlockTransactionData;
|
||||
import org.qortal.data.block.BlockArchiveData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
@@ -382,86 +383,6 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BlockSummaryData> getBlockSummaries(Integer startHeight, Integer endHeight, Integer count) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count ");
|
||||
|
||||
/*
|
||||
* start end count result
|
||||
* 10 40 null blocks 10 to 39 (excludes end block, ignore count)
|
||||
*
|
||||
* null null null blocks 1 to 50 (assume count=50, maybe start=1)
|
||||
* 30 null null blocks 30 to 79 (assume count=50)
|
||||
* 30 null 10 blocks 30 to 39
|
||||
*
|
||||
* null null 50 last 50 blocks? so if max(blocks.height) is 200, then blocks 151 to 200
|
||||
* null 200 null blocks 150 to 199 (excludes end block, assume count=50)
|
||||
* null 200 10 blocks 190 to 199 (excludes end block)
|
||||
*/
|
||||
|
||||
if (startHeight != null && endHeight != null) {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(startHeight);
|
||||
bindParams.add(Integer.valueOf(endHeight - 1));
|
||||
} else if (endHeight != null || (startHeight == null && count != null)) {
|
||||
// we are going to return blocks from the end of the chain
|
||||
if (count == null)
|
||||
count = 50;
|
||||
|
||||
if (endHeight == null) {
|
||||
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
|
||||
sql.append("JOIN Blocks ON height BETWEEN (max_height - ? + 1) AND max_height ");
|
||||
bindParams.add(count);
|
||||
} else {
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(Integer.valueOf(endHeight - count));
|
||||
bindParams.add(Integer.valueOf(endHeight - 1));
|
||||
}
|
||||
|
||||
} else {
|
||||
// we are going to return blocks from the start of the chain
|
||||
if (startHeight == null)
|
||||
startHeight = 1;
|
||||
|
||||
if (count == null)
|
||||
count = 50;
|
||||
|
||||
sql.append("FROM Blocks ");
|
||||
sql.append("WHERE height BETWEEN ? AND ?");
|
||||
bindParams.add(startHeight);
|
||||
bindParams.add(Integer.valueOf(startHeight + count - 1));
|
||||
}
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
if (resultSet == null)
|
||||
return blockSummaries;
|
||||
|
||||
do {
|
||||
byte[] signature = resultSet.getBytes(1);
|
||||
int height = resultSet.getInt(2);
|
||||
byte[] minterPublicKey = resultSet.getBytes(3);
|
||||
int onlineAccountsCount = resultSet.getInt(4);
|
||||
long timestamp = resultSet.getLong(5);
|
||||
int transactionCount = resultSet.getInt(6);
|
||||
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount,
|
||||
timestamp, transactionCount);
|
||||
blockSummaries.add(blockSummary);
|
||||
} while (resultSet.next());
|
||||
|
||||
return blockSummaries;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch height-ranged block summaries from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOnlineAccountsSignaturesTrimHeight() throws DataException {
|
||||
String sql = "SELECT online_signatures_trim_height FROM DatabaseInfo";
|
||||
|
@@ -0,0 +1,86 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.repository.BlockArchiveWriter;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* When switching to an archiving node, we need to archive most of the database contents.
|
||||
* This involves copying its data into flat files.
|
||||
* If we do this entirely as a background process, it is very slow and can interfere with syncing.
|
||||
* However, if we take the approach of doing this in bulk, before starting up the rest of the
|
||||
* processes, this makes it much faster and less invasive.
|
||||
*
|
||||
* From that point, the original background archiving process will run, but can be dialled right down
|
||||
* so not to interfere with syncing.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
public class HSQLDBDatabaseArchiving {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBDatabaseArchiving.class);
|
||||
|
||||
|
||||
public static boolean buildBlockArchive() throws DataException {
|
||||
try (final HSQLDBRepository repository = (HSQLDBRepository)RepositoryManager.getRepository()) {
|
||||
|
||||
// Only build the archive if we have never done so before
|
||||
int archiveHeight = repository.getBlockArchiveRepository().getBlockArchiveHeight();
|
||||
if (archiveHeight > 0) {
|
||||
// Already archived
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.info("Building block archive - this process could take a while... (approx. 15 mins on high spec)");
|
||||
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
int startHeight = 0;
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
try {
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(startHeight, maximumArchiveHeight, repository);
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
switch (result) {
|
||||
case OK:
|
||||
// Increment block archive height
|
||||
startHeight += writer.getWrittenCount();
|
||||
repository.getBlockArchiveRepository().setBlockArchiveHeight(startHeight);
|
||||
repository.saveChanges();
|
||||
break;
|
||||
|
||||
case STOPPING:
|
||||
return false;
|
||||
|
||||
case NOT_ENOUGH_BLOCKS:
|
||||
// We've reached the limit of the blocks we can archive
|
||||
// Return from the whole method
|
||||
return true;
|
||||
|
||||
case BLOCK_NOT_FOUND:
|
||||
// We tried to archive a block that didn't exist. This is a major failure and likely means
|
||||
// that a bootstrap or re-sync is needed. Return rom the method
|
||||
LOGGER.info("Error: block not found when building archive. If this error persists, " +
|
||||
"a bootstrap or re-sync may be needed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (IOException | TransformationException | InterruptedException e) {
|
||||
LOGGER.info("Caught exception when creating block cache", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far then something went wrong (most likely the app is stopping)
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.BlockArchiveWriter;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
@@ -36,6 +37,7 @@ public class HSQLDBDatabasePruning {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBDatabasePruning.class);
|
||||
|
||||
|
||||
public static boolean pruneATStates() throws SQLException, DataException {
|
||||
try (final HSQLDBRepository repository = (HSQLDBRepository)RepositoryManager.getRepository()) {
|
||||
|
||||
@@ -46,7 +48,18 @@ public class HSQLDBDatabasePruning {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.info("Starting bulk prune of AT states - this process could take a while... (approx. 2 mins on high spec)");
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Only proceed if we can see that the archiver has already finished
|
||||
// This way, if the archiver failed for any reason, we can prune once it has had
|
||||
// some opportunities to try again
|
||||
boolean upToDate = BlockArchiveWriter.isArchiverUpToDate(repository);
|
||||
if (!upToDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("Starting bulk prune of AT states - this process could take a while... " +
|
||||
"(approx. 2 mins on high spec, or upwards of 30 mins in some cases)");
|
||||
|
||||
// Create new AT-states table to hold smaller dataset
|
||||
repository.executeCheckedUpdate("DROP TABLE IF EXISTS ATStatesNew");
|
||||
@@ -58,6 +71,11 @@ public class HSQLDBDatabasePruning {
|
||||
repository.executeCheckedUpdate("SET TABLE ATStatesNew NEW SPACE");
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
// Add a height index
|
||||
LOGGER.info("Adding index to AT states table...");
|
||||
repository.executeCheckedUpdate("CREATE INDEX IF NOT EXISTS ATStatesNewHeightIndex ON ATStatesNew (height)");
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
|
||||
// Find our latest block
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
@@ -68,11 +86,21 @@ public class HSQLDBDatabasePruning {
|
||||
|
||||
// Calculate some constants for later use
|
||||
final int blockchainHeight = latestBlock.getHeight();
|
||||
final int maximumBlockToTrim = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
int maximumBlockToTrim = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Archive mode - don't prune anything that hasn't been archived yet
|
||||
maximumBlockToTrim = Math.min(maximumBlockToTrim, repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1);
|
||||
}
|
||||
final int startHeight = maximumBlockToTrim;
|
||||
final int endHeight = blockchainHeight;
|
||||
final int blockStep = 10000;
|
||||
|
||||
|
||||
// It's essential that we rebuild the latest AT states here, as we are using this data in the next query.
|
||||
// Failing to do this will result in important AT states being deleted, rendering the database unusable.
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
|
||||
|
||||
// Loop through all the LatestATStates and copy them to the new table
|
||||
LOGGER.info("Copying AT states...");
|
||||
for (int height = 0; height < endHeight; height += blockStep) {
|
||||
@@ -99,7 +127,7 @@ public class HSQLDBDatabasePruning {
|
||||
}
|
||||
|
||||
if (height >= startHeight) {
|
||||
// Now copy this AT states for each recent block it is present in
|
||||
// Now copy this AT's states for each recent block they is present in
|
||||
for (int i = startHeight; i < endHeight; i++) {
|
||||
if (latestAtHeight < i) {
|
||||
// This AT finished before this block so there is nothing to copy
|
||||
@@ -130,15 +158,12 @@ public class HSQLDBDatabasePruning {
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
// Add a height index
|
||||
LOGGER.info("Rebuilding AT states height index in repository");
|
||||
repository.executeCheckedUpdate("CREATE INDEX IF NOT EXISTS ATStatesHeightIndex ON ATStatesNew (height)");
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
// Finally, drop the original table and rename
|
||||
LOGGER.info("Deleting old AT states...");
|
||||
repository.executeCheckedUpdate("DROP TABLE ATStates");
|
||||
repository.executeCheckedUpdate("ALTER TABLE ATStatesNew RENAME TO ATStates");
|
||||
repository.executeCheckedUpdate("ALTER INDEX ATStatesNewHeightIndex RENAME TO ATStatesHeightIndex");
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
// Update the prune height
|
||||
@@ -159,20 +184,25 @@ public class HSQLDBDatabasePruning {
|
||||
private static boolean pruneATStateData() throws SQLException, DataException {
|
||||
try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Don't prune ATStatesData in archive mode
|
||||
return true;
|
||||
}
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
if (latestBlock == null) {
|
||||
LOGGER.info("Unable to determine blockchain height, necessary for bulk ATStatesData pruning");
|
||||
return false;
|
||||
}
|
||||
final int blockchainHeight = latestBlock.getHeight();
|
||||
final int upperPrunableHeight = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
int upperPrunableHeight = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
// ATStateData is already trimmed - so carry on from where we left off in the past
|
||||
int pruneStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
LOGGER.info("Starting bulk prune of AT states data - this process could take a while... (approx. 3 mins on high spec)");
|
||||
|
||||
while (pruneStartHeight < upperPrunableHeight) {
|
||||
// Prune all AT state data up until our latest minus pruneBlockLimit
|
||||
// Prune all AT state data up until our latest minus pruneBlockLimit (or our archive height)
|
||||
|
||||
if (Controller.isStopping()) {
|
||||
return false;
|
||||
@@ -190,19 +220,17 @@ public class HSQLDBDatabasePruning {
|
||||
repository.saveChanges();
|
||||
|
||||
if (numATStatesPruned > 0) {
|
||||
final int finalPruneStartHeight = pruneStartHeight;
|
||||
LOGGER.trace(() -> String.format("Pruned %d AT states data rows between blocks %d and %d",
|
||||
numATStatesPruned, finalPruneStartHeight, upperPruneHeight));
|
||||
LOGGER.trace(String.format("Pruned %d AT states data rows between blocks %d and %d",
|
||||
numATStatesPruned, pruneStartHeight, upperPruneHeight));
|
||||
} else {
|
||||
repository.getATRepository().setAtTrimHeight(upperBatchHeight);
|
||||
// No need to rebuild the latest AT states as we aren't currently synchronizing
|
||||
repository.saveChanges();
|
||||
LOGGER.debug(String.format("Bumping AT states trim height to %d", upperBatchHeight));
|
||||
|
||||
// Can we move onto next batch?
|
||||
if (upperPrunableHeight > upperBatchHeight) {
|
||||
pruneStartHeight = upperBatchHeight;
|
||||
repository.getATRepository().setAtTrimHeight(pruneStartHeight);
|
||||
// No need to rebuild the latest AT states as we aren't currently synchronizing
|
||||
repository.saveChanges();
|
||||
|
||||
final int finalPruneStartHeight = pruneStartHeight;
|
||||
LOGGER.debug(() -> String.format("Bumping AT states trim height to %d", finalPruneStartHeight));
|
||||
}
|
||||
else {
|
||||
// We've finished pruning
|
||||
@@ -225,15 +253,30 @@ public class HSQLDBDatabasePruning {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Only proceed if we can see that the archiver has already finished
|
||||
// This way, if the archiver failed for any reason, we can prune once it has had
|
||||
// some opportunities to try again
|
||||
boolean upToDate = BlockArchiveWriter.isArchiverUpToDate(repository);
|
||||
if (!upToDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
if (latestBlock == null) {
|
||||
LOGGER.info("Unable to determine blockchain height, necessary for bulk block pruning");
|
||||
return false;
|
||||
}
|
||||
final int blockchainHeight = latestBlock.getHeight();
|
||||
final int upperPrunableHeight = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
int upperPrunableHeight = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
int pruneStartHeight = 0;
|
||||
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Archive mode - don't prune anything that hasn't been archived yet
|
||||
upperPrunableHeight = Math.min(upperPrunableHeight, repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1);
|
||||
}
|
||||
|
||||
LOGGER.info("Starting bulk prune of blocks - this process could take a while... (approx. 5 mins on high spec)");
|
||||
|
||||
while (pruneStartHeight < upperPrunableHeight) {
|
||||
@@ -248,19 +291,17 @@ public class HSQLDBDatabasePruning {
|
||||
repository.saveChanges();
|
||||
|
||||
if (numBlocksPruned > 0) {
|
||||
final int finalPruneStartHeight = pruneStartHeight;
|
||||
LOGGER.info(() -> String.format("Pruned %d block%s between %d and %d",
|
||||
LOGGER.info(String.format("Pruned %d block%s between %d and %d",
|
||||
numBlocksPruned, (numBlocksPruned != 1 ? "s" : ""),
|
||||
finalPruneStartHeight, upperPruneHeight));
|
||||
pruneStartHeight, upperPruneHeight));
|
||||
} else {
|
||||
repository.getBlockRepository().setBlockPruneHeight(upperBatchHeight);
|
||||
repository.saveChanges();
|
||||
LOGGER.debug(String.format("Bumping block base prune height to %d", upperBatchHeight));
|
||||
|
||||
// Can we move onto next batch?
|
||||
if (upperPrunableHeight > upperBatchHeight) {
|
||||
pruneStartHeight = upperBatchHeight;
|
||||
repository.getBlockRepository().setBlockPruneHeight(pruneStartHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
final int finalPruneStartHeight = pruneStartHeight;
|
||||
LOGGER.debug(() -> String.format("Bumping block base prune height to %d", finalPruneStartHeight));
|
||||
}
|
||||
else {
|
||||
// We've finished pruning
|
||||
|
@@ -873,6 +873,25 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("ALTER TABLE DatabaseInfo ADD block_prune_height INT NOT NULL DEFAULT 0");
|
||||
break;
|
||||
|
||||
case 36:
|
||||
// Block archive support
|
||||
stmt.execute("ALTER TABLE DatabaseInfo ADD block_archive_height INT NOT NULL DEFAULT 0");
|
||||
|
||||
// Block archive (lookup table to map signature to height)
|
||||
// Actual data is stored in archive files outside of the database
|
||||
stmt.execute("CREATE TABLE BlockArchive (signature BlockSignature, height INTEGER NOT NULL, "
|
||||
+ "minted_when EpochMillis NOT NULL, minter QortalPublicKey NOT NULL, "
|
||||
+ "PRIMARY KEY (signature))");
|
||||
// For finding blocks by height.
|
||||
stmt.execute("CREATE INDEX BlockArchiveHeightIndex ON BlockArchive (height)");
|
||||
// For finding blocks by the account that minted them.
|
||||
stmt.execute("CREATE INDEX BlockArchiveMinterIndex ON BlockArchive (minter)");
|
||||
// For finding blocks by timestamp or finding height of latest block immediately before timestamp, etc.
|
||||
stmt.execute("CREATE INDEX BlockArchiveTimestampHeightIndex ON BlockArchive (minted_when, height)");
|
||||
// Use a separate table space as this table will be very large.
|
||||
stmt.execute("SET TABLE BlockArchive NEW SPACE");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@@ -31,22 +31,7 @@ import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.gui.SysTray;
|
||||
import org.qortal.repository.ATRepository;
|
||||
import org.qortal.repository.AccountRepository;
|
||||
import org.qortal.repository.ArbitraryRepository;
|
||||
import org.qortal.repository.AssetRepository;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.ChatRepository;
|
||||
import org.qortal.repository.CrossChainRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.MessageRepository;
|
||||
import org.qortal.repository.NameRepository;
|
||||
import org.qortal.repository.NetworkRepository;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.TransactionRepository;
|
||||
import org.qortal.repository.VotingRepository;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.repository.hsqldb.transaction.HSQLDBTransactionRepository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
@@ -69,12 +54,14 @@ public class HSQLDBRepository implements Repository {
|
||||
protected final Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
|
||||
// We want the same object corresponding to the actual DB
|
||||
protected final Object trimHeightsLock = RepositoryManager.getRepositoryFactory();
|
||||
protected final Object latestATStatesLock = RepositoryManager.getRepositoryFactory();
|
||||
|
||||
private final ATRepository atRepository = new HSQLDBATRepository(this);
|
||||
private final AccountRepository accountRepository = new HSQLDBAccountRepository(this);
|
||||
private final ArbitraryRepository arbitraryRepository = new HSQLDBArbitraryRepository(this);
|
||||
private final AssetRepository assetRepository = new HSQLDBAssetRepository(this);
|
||||
private final BlockRepository blockRepository = new HSQLDBBlockRepository(this);
|
||||
private final BlockArchiveRepository blockArchiveRepository = new HSQLDBBlockArchiveRepository(this);
|
||||
private final ChatRepository chatRepository = new HSQLDBChatRepository(this);
|
||||
private final CrossChainRepository crossChainRepository = new HSQLDBCrossChainRepository(this);
|
||||
private final GroupRepository groupRepository = new HSQLDBGroupRepository(this);
|
||||
@@ -142,6 +129,11 @@ public class HSQLDBRepository implements Repository {
|
||||
return this.blockRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockArchiveRepository getBlockArchiveRepository() {
|
||||
return this.blockArchiveRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatRepository getChatRepository() {
|
||||
return this.chatRepository;
|
||||
|
@@ -68,6 +68,9 @@ public class Settings {
|
||||
};
|
||||
private Boolean apiRestricted;
|
||||
private String apiKey = null;
|
||||
/** Whether to disable API key or loopback address checking
|
||||
* IMPORTANT: do not disable for shared nodes or low-security local networks */
|
||||
private boolean apiKeyDisabled = false;
|
||||
private boolean apiLoggingEnabled = false;
|
||||
private boolean apiDocumentationEnabled = false;
|
||||
// Both of these need to be set for API to use SSL
|
||||
@@ -94,7 +97,7 @@ public class Settings {
|
||||
private int blockCacheSize = 10;
|
||||
|
||||
/** How long to keep old, full, AT state data (ms). */
|
||||
private long atStatesMaxLifetime = 2 * 7 * 24 * 60 * 60 * 1000L; // milliseconds
|
||||
private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds
|
||||
/** How often to attempt AT state trimming (ms). */
|
||||
private long atStatesTrimInterval = 5678L; // milliseconds
|
||||
/** Block height range to scan for trimmable AT states.<br>
|
||||
@@ -129,6 +132,12 @@ public class Settings {
|
||||
private int blockPruneBatchSize = 10000; // blocks
|
||||
|
||||
|
||||
/** Whether we should archive old data to reduce the database size */
|
||||
private boolean archiveEnabled = true;
|
||||
/** How often to attempt archiving (ms). */
|
||||
private long archiveInterval = 7171L; // milliseconds
|
||||
|
||||
|
||||
// Peer-to-peer related
|
||||
private boolean isTestNet = false;
|
||||
/** Port number for inbound peer-to-peer connections. */
|
||||
@@ -376,6 +385,10 @@ public class Settings {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
public boolean isApiKeyDisabled() {
|
||||
return this.apiKeyDisabled;
|
||||
}
|
||||
|
||||
public boolean isApiLoggingEnabled() {
|
||||
return this.apiLoggingEnabled;
|
||||
}
|
||||
@@ -574,4 +587,13 @@ public class Settings {
|
||||
return this.blockPruneBatchSize;
|
||||
}
|
||||
|
||||
|
||||
public boolean isArchiveEnabled() {
|
||||
return this.archiveEnabled;
|
||||
}
|
||||
|
||||
public long getArchiveInterval() {
|
||||
return this.archiveInterval;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -48,6 +48,11 @@ public class AccountFlagsTransaction extends Transaction {
|
||||
return ValidationResult.NO_FLAG_PERMISSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
Account target = this.getTarget();
|
||||
|
@@ -49,6 +49,11 @@ public class AccountLevelTransaction extends Transaction {
|
||||
return ValidationResult.NO_FLAG_PERMISSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
Account target = getTarget();
|
||||
|
@@ -84,6 +84,11 @@ public class AddGroupAdminTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group adminship
|
||||
@@ -98,4 +103,4 @@ public class AddGroupAdminTransaction extends Transaction {
|
||||
group.unpromoteToAdmin(this.addGroupAdminTransactionData);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -60,6 +60,11 @@ public class ArbitraryTransaction extends Transaction {
|
||||
arbitraryTransactionData.getFee());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Wrap and delegate payment processing to Payment class.
|
||||
|
@@ -80,6 +80,11 @@ public class AtTransaction extends Transaction {
|
||||
return Arrays.equals(atAccount.getLastReference(), atTransactionData.getReference());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Check recipient address is valid
|
||||
|
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.BuyNameTransactionData;
|
||||
@@ -98,6 +99,17 @@ public class BuyNameTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
|
||||
|
||||
// Rebuild this name in the Names table from the transaction history
|
||||
// This is necessary because in some rare cases names can be missing from the Names table after registration
|
||||
// but we have been unable to reproduce the issue and track down the root cause
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildName(buyNameTransactionData.getName(), this.repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Buy Name
|
||||
|
@@ -62,6 +62,11 @@ public class CancelAssetOrderTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Mark Order as completed so no more trades can happen
|
||||
|
@@ -83,6 +83,11 @@ public class CancelGroupBanTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group Membership
|
||||
|
@@ -83,6 +83,11 @@ public class CancelGroupInviteTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group Membership
|
||||
|
@@ -79,6 +79,11 @@ public class CancelSellNameTransaction extends Transaction {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Name
|
||||
|
@@ -135,6 +135,11 @@ public class ChatTransaction extends Transaction {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
|
@@ -135,6 +135,11 @@ public class CreateAssetOrderTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Order Id is transaction's signature
|
||||
|
@@ -92,6 +92,11 @@ public class CreateGroupTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Create Group
|
||||
|
@@ -106,6 +106,11 @@ public class CreatePollTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Publish poll to allow voting
|
||||
|
@@ -203,6 +203,11 @@ public class DeployAtTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
ensureATAddress(this.deployAtTransactionData);
|
||||
|
@@ -100,6 +100,11 @@ public class GenesisTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
Account recipient = new Account(repository, this.genesisTransactionData.getRecipient());
|
||||
|
@@ -66,6 +66,11 @@ public class GroupApprovalTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Find previous approval decision (if any) by this admin for pending transaction
|
||||
|
@@ -87,6 +87,11 @@ public class GroupBanTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group Membership
|
||||
|
@@ -88,6 +88,11 @@ public class GroupInviteTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group Membership
|
||||
|
@@ -89,6 +89,11 @@ public class GroupKickTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group Membership
|
||||
|
@@ -92,6 +92,11 @@ public class IssueAssetTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Issue asset
|
||||
|
@@ -67,6 +67,11 @@ public class JoinGroupTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group Membership
|
||||
|
@@ -67,6 +67,11 @@ public class LeaveGroupTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group Membership
|
||||
|
@@ -239,6 +239,11 @@ public class MessageTransaction extends Transaction {
|
||||
getPaymentData(), this.messageTransactionData.getFee(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// If we have no amount then there's nothing to do
|
||||
|
@@ -67,6 +67,11 @@ public class MultiPaymentTransaction extends Transaction {
|
||||
return new Payment(this.repository).isProcessable(this.multiPaymentTransactionData.getSenderPublicKey(), payments, this.multiPaymentTransactionData.getFee());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Wrap and delegate payment processing to Payment class.
|
||||
|
@@ -61,6 +61,11 @@ public class PaymentTransaction extends Transaction {
|
||||
return new Payment(this.repository).isProcessable(this.paymentTransactionData.getSenderPublicKey(), getPaymentData(), this.paymentTransactionData.getFee());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Wrap and delegate payment processing to Payment class.
|
||||
|
@@ -149,6 +149,11 @@ public class PresenceTransaction extends Transaction {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
|
@@ -80,6 +80,11 @@ public class PublicizeTransaction extends Transaction {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// There can be only one
|
||||
|
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.RegisterNameTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -88,6 +89,17 @@ public class RegisterNameTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData;
|
||||
|
||||
// Rebuild this name in the Names table from the transaction history
|
||||
// This is necessary because in some rare cases names can be missing from the Names table after registration
|
||||
// but we have been unable to reproduce the issue and track down the root cause
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildName(registerNameTransactionData.getName(), this.repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Register Name
|
||||
|
@@ -87,6 +87,11 @@ public class RemoveGroupAdminTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group adminship
|
||||
@@ -107,4 +112,4 @@ public class RemoveGroupAdminTransaction extends Transaction {
|
||||
this.repository.getTransactionRepository().save(this.removeGroupAdminTransactionData);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -159,6 +159,11 @@ public class RewardShareTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
PublicKeyAccount mintingAccount = getMintingAccount();
|
||||
|
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.SellNameTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -89,6 +90,17 @@ public class SellNameTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
|
||||
|
||||
// Rebuild this name in the Names table from the transaction history
|
||||
// This is necessary because in some rare cases names can be missing from the Names table after registration
|
||||
// but we have been unable to reproduce the issue and track down the root cause
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildName(sellNameTransactionData.getName(), this.repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Sell Name
|
||||
|
@@ -56,6 +56,11 @@ public class SetGroupTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
Account creator = getCreator();
|
||||
|
@@ -791,6 +791,8 @@ public abstract class Transaction {
|
||||
// Fix up approval status
|
||||
this.setInitialApprovalStatus();
|
||||
|
||||
this.preProcess();
|
||||
|
||||
ValidationResult validationResult = this.isValidUnconfirmed();
|
||||
if (validationResult != ValidationResult.OK)
|
||||
return validationResult;
|
||||
@@ -891,6 +893,14 @@ public abstract class Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* * Pre-process a transaction before validating or processing the block
|
||||
* This allows for any database integrity checks prior to validation.
|
||||
*
|
||||
* @throws DataException
|
||||
*/
|
||||
public abstract void preProcess() throws DataException;
|
||||
|
||||
/**
|
||||
* Actually process a transaction, updating the blockchain.
|
||||
* <p>
|
||||
|
@@ -61,6 +61,11 @@ public class TransferAssetTransaction extends Transaction {
|
||||
return new Payment(this.repository).isProcessable(this.transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), this.transferAssetTransactionData.getFee());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Wrap asset transfer as a payment and delegate processing to Payment class.
|
||||
|
@@ -68,6 +68,11 @@ public class TransferPrivsTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
Account sender = this.getSender();
|
||||
|
@@ -90,6 +90,11 @@ public class UpdateAssetTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Asset
|
||||
|
@@ -109,6 +109,11 @@ public class UpdateGroupTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Group
|
||||
|
@@ -2,9 +2,11 @@ package org.qortal.transaction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -124,6 +126,22 @@ public class UpdateNameTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
||||
|
||||
// Rebuild this name in the Names table from the transaction history
|
||||
// This is necessary because in some rare cases names can be missing from the Names table after registration
|
||||
// but we have been unable to reproduce the issue and track down the root cause
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildName(updateNameTransactionData.getName(), this.repository);
|
||||
|
||||
if (!Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName())) {
|
||||
// Renaming - so make sure the new name is rebuilt too
|
||||
namesDatabaseIntegrityCheck.rebuildName(updateNameTransactionData.getNewName(), this.repository);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
// Update Name
|
||||
|
@@ -92,6 +92,11 @@ public class VoteOnPollTransaction extends Transaction {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
String pollName = this.voteOnPollTransactionData.getPollName();
|
||||
|
78
src/main/java/org/qortal/utils/BlockArchiveUtils.java
Normal file
78
src/main/java/org/qortal/utils/BlockArchiveUtils.java
Normal file
@@ -0,0 +1,78 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockArchiveReader;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BlockArchiveUtils {
|
||||
|
||||
/**
|
||||
* importFromArchive
|
||||
* <p>
|
||||
* Reads the requested block range from the archive
|
||||
* and imports the BlockData and AT state data hashes
|
||||
* This can be used to convert a block archive back
|
||||
* into the HSQLDB, in order to make it SQL-compatible
|
||||
* again.
|
||||
* <p>
|
||||
* Note: calls discardChanges() and saveChanges(), so
|
||||
* make sure that you commit any existing repository
|
||||
* changes before calling this method.
|
||||
*
|
||||
* @param startHeight The earliest block to import
|
||||
* @param endHeight The latest block to import
|
||||
* @param repository A clean repository session
|
||||
* @throws DataException
|
||||
*/
|
||||
public static void importFromArchive(int startHeight, int endHeight, Repository repository) throws DataException {
|
||||
repository.discardChanges();
|
||||
final int requestedRange = endHeight+1-startHeight;
|
||||
|
||||
List<Triple<BlockData, List<TransactionData>, List<ATStateData>>> blockInfoList =
|
||||
BlockArchiveReader.getInstance().fetchBlocksFromRange(startHeight, endHeight);
|
||||
|
||||
// Ensure that we have received all of the requested blocks
|
||||
if (blockInfoList == null || blockInfoList.isEmpty()) {
|
||||
throw new IllegalStateException("No blocks found when importing from archive");
|
||||
}
|
||||
if (blockInfoList.size() != requestedRange) {
|
||||
throw new IllegalStateException("Non matching block count when importing from archive");
|
||||
}
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> firstBlock = blockInfoList.get(0);
|
||||
if (firstBlock == null || firstBlock.getA().getHeight() != startHeight) {
|
||||
throw new IllegalStateException("Non matching first block when importing from archive");
|
||||
}
|
||||
if (blockInfoList.size() > 0) {
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> lastBlock =
|
||||
blockInfoList.get(blockInfoList.size() - 1);
|
||||
if (lastBlock == null || lastBlock.getA().getHeight() != endHeight) {
|
||||
throw new IllegalStateException("Non matching last block when importing from archive");
|
||||
}
|
||||
}
|
||||
|
||||
// Everything seems okay, so go ahead with the import
|
||||
for (Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo : blockInfoList) {
|
||||
try {
|
||||
// Save block
|
||||
repository.getBlockRepository().save(blockInfo.getA());
|
||||
|
||||
// Save AT state data hashes
|
||||
for (ATStateData atStateData : blockInfo.getC()) {
|
||||
atStateData.setHeight(blockInfo.getA().getHeight());
|
||||
repository.getATRepository().save(atStateData);
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
repository.discardChanges();
|
||||
throw new IllegalStateException("Unable to import blocks from archive");
|
||||
}
|
||||
}
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
}
|
@@ -11,8 +11,8 @@
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 6,
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 2592000000,
|
||||
"onlineAccountSignaturesMaxLifetime": 3196800000,
|
||||
"onlineAccountSignaturesMinLifetime": 43200000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 5.00 },
|
||||
{ "height": 259201, "reward": 4.75 },
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 62 KiB |
525
src/test/java/org/qortal/test/BlockArchiveTests.java
Normal file
525
src/test/java/org/qortal/test/BlockArchiveTests.java
Normal file
@@ -0,0 +1,525 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.test.common.AtUtils;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.utils.BlockArchiveUtils;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BlockArchiveTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings(); // Necessary to set NTP offset
|
||||
Common.useSettings("test-settings-v2-block-archive.json");
|
||||
this.deleteArchiveDirectory();
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() throws DataException {
|
||||
this.deleteArchiveDirectory();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testWriter() throws DataException, InterruptedException, TransformationException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Alice self share online
|
||||
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
mintingAndOnlineAccounts.add(aliceSelfShare);
|
||||
|
||||
// Mint some blocks so that we are able to archive them later
|
||||
for (int i = 0; i < 1000; i++)
|
||||
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
|
||||
// 900 blocks are trimmed (this specifies the first untrimmed height)
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901);
|
||||
repository.getATRepository().setAtTrimHeight(901);
|
||||
|
||||
// Check the max archive height - this should be one less than the first untrimmed height
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
assertEquals(900, maximumArchiveHeight);
|
||||
|
||||
// Write blocks 2-900 to the archive
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository);
|
||||
writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result);
|
||||
|
||||
// Make sure that the archive contains the correct number of blocks
|
||||
assertEquals(900 - 1, writer.getWrittenCount());
|
||||
|
||||
// Increment block archive height
|
||||
repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount());
|
||||
repository.saveChanges();
|
||||
assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Ensure the file exists
|
||||
File outputFile = writer.getOutputPath().toFile();
|
||||
assertTrue(outputFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriterAndReader() throws DataException, InterruptedException, TransformationException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Alice self share online
|
||||
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
mintingAndOnlineAccounts.add(aliceSelfShare);
|
||||
|
||||
// Mint some blocks so that we are able to archive them later
|
||||
for (int i = 0; i < 1000; i++)
|
||||
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
|
||||
// 900 blocks are trimmed (this specifies the first untrimmed height)
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901);
|
||||
repository.getATRepository().setAtTrimHeight(901);
|
||||
|
||||
// Check the max archive height - this should be one less than the first untrimmed height
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
assertEquals(900, maximumArchiveHeight);
|
||||
|
||||
// Write blocks 2-900 to the archive
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository);
|
||||
writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result);
|
||||
|
||||
// Make sure that the archive contains the correct number of blocks
|
||||
assertEquals(900 - 1, writer.getWrittenCount());
|
||||
|
||||
// Increment block archive height
|
||||
repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount());
|
||||
repository.saveChanges();
|
||||
assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Ensure the file exists
|
||||
File outputFile = writer.getOutputPath().toFile();
|
||||
assertTrue(outputFile.exists());
|
||||
|
||||
// Read block 2 from the archive
|
||||
BlockArchiveReader reader = BlockArchiveReader.getInstance();
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> block2Info = reader.fetchBlockAtHeight(2);
|
||||
BlockData block2ArchiveData = block2Info.getA();
|
||||
|
||||
// Read block 2 from the repository
|
||||
BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2);
|
||||
|
||||
// Ensure the values match
|
||||
assertEquals(block2ArchiveData.getHeight(), block2RepositoryData.getHeight());
|
||||
assertArrayEquals(block2ArchiveData.getSignature(), block2RepositoryData.getSignature());
|
||||
|
||||
// Test some values in the archive
|
||||
assertEquals(1, block2ArchiveData.getOnlineAccountsCount());
|
||||
|
||||
// Read block 900 from the archive
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> block900Info = reader.fetchBlockAtHeight(900);
|
||||
BlockData block900ArchiveData = block900Info.getA();
|
||||
|
||||
// Read block 900 from the repository
|
||||
BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900);
|
||||
|
||||
// Ensure the values match
|
||||
assertEquals(block900ArchiveData.getHeight(), block900RepositoryData.getHeight());
|
||||
assertArrayEquals(block900ArchiveData.getSignature(), block900RepositoryData.getSignature());
|
||||
|
||||
// Test some values in the archive
|
||||
assertEquals(1, block900ArchiveData.getOnlineAccountsCount());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArchivedAtStates() throws DataException, InterruptedException, TransformationException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Alice self share online
|
||||
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
mintingAndOnlineAccounts.add(aliceSelfShare);
|
||||
|
||||
// Deploy an AT so that we have AT state data
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint some blocks so that we are able to archive them later
|
||||
for (int i = 0; i < 10; i++)
|
||||
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
|
||||
// 9 blocks are trimmed (this specifies the first untrimmed height)
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(10);
|
||||
repository.getATRepository().setAtTrimHeight(10);
|
||||
|
||||
// Check the max archive height
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
assertEquals(9, maximumArchiveHeight);
|
||||
|
||||
// Write blocks 2-9 to the archive
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository);
|
||||
writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result);
|
||||
|
||||
// Make sure that the archive contains the correct number of blocks
|
||||
assertEquals(9 - 1, writer.getWrittenCount());
|
||||
|
||||
// Increment block archive height
|
||||
repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount());
|
||||
repository.saveChanges();
|
||||
assertEquals(9 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Ensure the file exists
|
||||
File outputFile = writer.getOutputPath().toFile();
|
||||
assertTrue(outputFile.exists());
|
||||
|
||||
// Check blocks 3-9
|
||||
for (Integer testHeight = 2; testHeight <= 9; testHeight++) {
|
||||
|
||||
// Read a block from the archive
|
||||
BlockArchiveReader reader = BlockArchiveReader.getInstance();
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = reader.fetchBlockAtHeight(testHeight);
|
||||
BlockData archivedBlockData = blockInfo.getA();
|
||||
ATStateData archivedAtStateData = blockInfo.getC().isEmpty() ? null : blockInfo.getC().get(0);
|
||||
List<TransactionData> archivedTransactions = blockInfo.getB();
|
||||
|
||||
// Read the same block from the repository
|
||||
BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight);
|
||||
ATStateData repositoryAtStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
||||
// Ensure the repository has full AT state data
|
||||
assertNotNull(repositoryAtStateData.getStateHash());
|
||||
assertNotNull(repositoryAtStateData.getStateData());
|
||||
|
||||
// Check the archived AT state
|
||||
if (testHeight == 2) {
|
||||
// Block 2 won't have an AT state hash because it's initial (and has the DEPLOY_AT in the same block)
|
||||
assertNull(archivedAtStateData);
|
||||
|
||||
assertEquals(1, archivedTransactions.size());
|
||||
assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType());
|
||||
}
|
||||
else {
|
||||
// For blocks 3+, ensure the archive has the AT state data, but not the hashes
|
||||
assertNotNull(archivedAtStateData.getStateHash());
|
||||
assertNull(archivedAtStateData.getStateData());
|
||||
|
||||
// They also shouldn't have any transactions
|
||||
assertTrue(archivedTransactions.isEmpty());
|
||||
}
|
||||
|
||||
// Also check the online accounts count and height
|
||||
assertEquals(1, archivedBlockData.getOnlineAccountsCount());
|
||||
assertEquals(testHeight, archivedBlockData.getHeight());
|
||||
|
||||
// Ensure the values match
|
||||
assertEquals(archivedBlockData.getHeight(), repositoryBlockData.getHeight());
|
||||
assertArrayEquals(archivedBlockData.getSignature(), repositoryBlockData.getSignature());
|
||||
assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount());
|
||||
assertArrayEquals(archivedBlockData.getMinterSignature(), repositoryBlockData.getMinterSignature());
|
||||
assertEquals(archivedBlockData.getATCount(), repositoryBlockData.getATCount());
|
||||
assertEquals(archivedBlockData.getOnlineAccountsCount(), repositoryBlockData.getOnlineAccountsCount());
|
||||
assertArrayEquals(archivedBlockData.getReference(), repositoryBlockData.getReference());
|
||||
assertEquals(archivedBlockData.getTimestamp(), repositoryBlockData.getTimestamp());
|
||||
assertEquals(archivedBlockData.getATFees(), repositoryBlockData.getATFees());
|
||||
assertEquals(archivedBlockData.getTotalFees(), repositoryBlockData.getTotalFees());
|
||||
assertEquals(archivedBlockData.getTransactionCount(), repositoryBlockData.getTransactionCount());
|
||||
assertArrayEquals(archivedBlockData.getTransactionsSignature(), repositoryBlockData.getTransactionsSignature());
|
||||
|
||||
if (testHeight != 2) {
|
||||
assertArrayEquals(archivedAtStateData.getStateHash(), repositoryAtStateData.getStateHash());
|
||||
}
|
||||
}
|
||||
|
||||
// Check block 10 (unarchived)
|
||||
BlockArchiveReader reader = BlockArchiveReader.getInstance();
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = reader.fetchBlockAtHeight(10);
|
||||
assertNull(blockInfo);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Alice self share online
|
||||
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
mintingAndOnlineAccounts.add(aliceSelfShare);
|
||||
|
||||
// Deploy an AT so that we have AT state data
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
long fundingAmount = 1_00000000L;
|
||||
AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint some blocks so that we are able to archive them later
|
||||
for (int i = 0; i < 1000; i++)
|
||||
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
|
||||
// Assume 900 blocks are trimmed (this specifies the first untrimmed height)
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(901);
|
||||
repository.getATRepository().setAtTrimHeight(901);
|
||||
|
||||
// Check the max archive height - this should be one less than the first untrimmed height
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
assertEquals(900, maximumArchiveHeight);
|
||||
|
||||
// Write blocks 2-900 to the archive
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository);
|
||||
writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result);
|
||||
|
||||
// Make sure that the archive contains the correct number of blocks
|
||||
assertEquals(900 - 1, writer.getWrittenCount());
|
||||
|
||||
// Increment block archive height
|
||||
repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount());
|
||||
repository.saveChanges();
|
||||
assertEquals(900 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Ensure the file exists
|
||||
File outputFile = writer.getOutputPath().toFile();
|
||||
assertTrue(outputFile.exists());
|
||||
|
||||
// Ensure the SQL repository contains blocks 2 and 900...
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(2));
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(900));
|
||||
|
||||
// Prune all the archived blocks
|
||||
int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 900);
|
||||
assertEquals(900-1, numBlocksPruned);
|
||||
repository.getBlockRepository().setBlockPruneHeight(901);
|
||||
|
||||
// Prune the AT states for the archived blocks
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900);
|
||||
assertEquals(900-1, numATStatesPruned);
|
||||
repository.getATRepository().setAtPruneHeight(901);
|
||||
|
||||
// Now ensure the SQL repository is missing blocks 2 and 900...
|
||||
assertNull(repository.getBlockRepository().fromHeight(2));
|
||||
assertNull(repository.getBlockRepository().fromHeight(900));
|
||||
|
||||
// ... but it's not missing blocks 1 and 901 (we don't prune the genesis block)
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(1));
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(901));
|
||||
|
||||
// Validate the latest block height in the repository
|
||||
assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Alice self share online
|
||||
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
mintingAndOnlineAccounts.add(aliceSelfShare);
|
||||
|
||||
// Deploy an AT so that we have AT state data
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
long fundingAmount = 1_00000000L;
|
||||
AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint some blocks so that we are able to archive them later
|
||||
for (int i = 0; i < 1000; i++)
|
||||
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
|
||||
// Make sure that block 500 has full AT state data and data hash
|
||||
List<ATStateData> block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500);
|
||||
ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500);
|
||||
assertNotNull(atStatesData.getStateHash());
|
||||
assertNotNull(atStatesData.getStateData());
|
||||
|
||||
// Trim the first 500 blocks
|
||||
repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500);
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501);
|
||||
repository.getATRepository().trimAtStates(0, 500, 1000);
|
||||
repository.getATRepository().setAtTrimHeight(501);
|
||||
|
||||
// Now block 500 should only have the AT state data hash
|
||||
block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500);
|
||||
atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500);
|
||||
assertNotNull(atStatesData.getStateHash());
|
||||
assertNull(atStatesData.getStateData());
|
||||
|
||||
// ... but block 501 should have the full data
|
||||
List<ATStateData> block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501);
|
||||
atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501);
|
||||
assertNotNull(atStatesData.getStateHash());
|
||||
assertNotNull(atStatesData.getStateData());
|
||||
|
||||
// Check the max archive height - this should be one less than the first untrimmed height
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
assertEquals(500, maximumArchiveHeight);
|
||||
|
||||
BlockData block3DataPreArchive = repository.getBlockRepository().fromHeight(3);
|
||||
|
||||
// Write blocks 2-500 to the archive
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(0, maximumArchiveHeight, repository);
|
||||
writer.setShouldEnforceFileSizeTarget(false); // To avoid the need to pre-calculate file sizes
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
assertEquals(BlockArchiveWriter.BlockArchiveWriteResult.OK, result);
|
||||
|
||||
// Make sure that the archive contains the correct number of blocks
|
||||
assertEquals(500 - 1, writer.getWrittenCount()); // -1 for the genesis block
|
||||
|
||||
// Increment block archive height
|
||||
repository.getBlockArchiveRepository().setBlockArchiveHeight(writer.getWrittenCount());
|
||||
repository.saveChanges();
|
||||
assertEquals(500 - 1, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Ensure the file exists
|
||||
File outputFile = writer.getOutputPath().toFile();
|
||||
assertTrue(outputFile.exists());
|
||||
|
||||
// Ensure the SQL repository contains blocks 2 and 500...
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(2));
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(500));
|
||||
|
||||
// Prune all the archived blocks
|
||||
int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 500);
|
||||
assertEquals(500-1, numBlocksPruned);
|
||||
repository.getBlockRepository().setBlockPruneHeight(501);
|
||||
|
||||
// Prune the AT states for the archived blocks
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500);
|
||||
assertEquals(499, numATStatesPruned);
|
||||
repository.getATRepository().setAtPruneHeight(501);
|
||||
|
||||
// Now ensure the SQL repository is missing blocks 2 and 500...
|
||||
assertNull(repository.getBlockRepository().fromHeight(2));
|
||||
assertNull(repository.getBlockRepository().fromHeight(500));
|
||||
|
||||
// ... but it's not missing blocks 1 and 501 (we don't prune the genesis block)
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(1));
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(501));
|
||||
|
||||
// Validate the latest block height in the repository
|
||||
assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight());
|
||||
|
||||
// Now orphan some unarchived blocks.
|
||||
BlockUtils.orphanBlocks(repository, 500);
|
||||
assertEquals(502, (int) repository.getBlockRepository().getLastBlock().getHeight());
|
||||
|
||||
// We're close to the lower limit of the SQL database now, so
|
||||
// we need to import some blocks from the archive
|
||||
BlockArchiveUtils.importFromArchive(401, 500, repository);
|
||||
|
||||
// Ensure the SQL repository now contains block 401 but not 400...
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(401));
|
||||
assertNull(repository.getBlockRepository().fromHeight(400));
|
||||
|
||||
// Import the remaining 399 blocks
|
||||
BlockArchiveUtils.importFromArchive(2, 400, repository);
|
||||
|
||||
// Verify that block 3 matches the original
|
||||
BlockData block3DataPostArchive = repository.getBlockRepository().fromHeight(3);
|
||||
assertArrayEquals(block3DataPreArchive.getSignature(), block3DataPostArchive.getSignature());
|
||||
assertEquals(block3DataPreArchive.getHeight(), block3DataPostArchive.getHeight());
|
||||
|
||||
// Orphan 1 more block, which should be the last one that is possible to be orphaned
|
||||
BlockUtils.orphanBlocks(repository, 1);
|
||||
|
||||
// Orphan another block, which should fail
|
||||
Exception exception = null;
|
||||
try {
|
||||
BlockUtils.orphanBlocks(repository, 1);
|
||||
} catch (DataException e) {
|
||||
exception = e;
|
||||
}
|
||||
|
||||
// Ensure that a DataException is thrown because there is no more AT states data available
|
||||
assertNotNull(exception);
|
||||
assertEquals(DataException.class, exception.getClass());
|
||||
|
||||
// FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception
|
||||
// and allow orphaning back through blocks with trimmed AT states.
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Many nodes are missing an ATStatesHeightIndex due to an earlier bug
|
||||
* In these cases we disable archiving and pruning as this index is a
|
||||
* very essential component in these processes.
|
||||
*/
|
||||
@Test
|
||||
public void testMissingAtStatesHeightIndex() throws DataException, SQLException {
|
||||
try (final HSQLDBRepository repository = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
|
||||
// Firstly check that we're able to prune or archive when the index exists
|
||||
assertTrue(repository.getATRepository().hasAtStatesHeightIndex());
|
||||
assertTrue(RepositoryManager.canArchiveOrPrune());
|
||||
|
||||
// Delete the index
|
||||
repository.prepareStatement("DROP INDEX ATSTATESHEIGHTINDEX").execute();
|
||||
|
||||
// Ensure check that we're unable to prune or archive when the index doesn't exist
|
||||
assertFalse(repository.getATRepository().hasAtStatesHeightIndex());
|
||||
assertFalse(RepositoryManager.canArchiveOrPrune());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void deleteArchiveDirectory() {
|
||||
// Delete archive directory if exists
|
||||
Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath();
|
||||
try {
|
||||
FileUtils.deleteDirectory(archivePath.toFile());
|
||||
} catch (IOException e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
91
src/test/java/org/qortal/test/PruneTests.java
Normal file
91
src/test/java/org/qortal/test/PruneTests.java
Normal file
@@ -0,0 +1,91 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.AtUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class PruneTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPruning() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Alice self share online
|
||||
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
|
||||
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
|
||||
mintingAndOnlineAccounts.add(aliceSelfShare);
|
||||
|
||||
// Deploy an AT so that we have AT state data
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
long fundingAmount = 1_00000000L;
|
||||
AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint some blocks
|
||||
for (int i = 2; i <= 10; i++)
|
||||
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
|
||||
|
||||
// Make sure that all blocks have full AT state data and data hash
|
||||
for (Integer i=2; i <= 10; i++) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(i);
|
||||
assertNotNull(blockData.getSignature());
|
||||
assertEquals(i, blockData.getHeight());
|
||||
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
|
||||
assertNotNull(atStatesDataList);
|
||||
assertFalse(atStatesDataList.isEmpty());
|
||||
ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(atStatesDataList.get(0).getATAddress(), i);
|
||||
assertNotNull(atStatesData.getStateHash());
|
||||
assertNotNull(atStatesData.getStateData());
|
||||
}
|
||||
|
||||
// Prune blocks 2-5
|
||||
int numBlocksPruned = repository.getBlockRepository().pruneBlocks(0, 5);
|
||||
assertEquals(4, numBlocksPruned);
|
||||
repository.getBlockRepository().setBlockPruneHeight(6);
|
||||
|
||||
// Prune AT states for blocks 2-5
|
||||
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 5);
|
||||
assertEquals(4, numATStatesPruned);
|
||||
repository.getATRepository().setAtPruneHeight(6);
|
||||
|
||||
// Make sure that blocks 2-5 are now missing block data and AT states data
|
||||
for (Integer i=2; i <= 5; i++) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(i);
|
||||
assertNull(blockData);
|
||||
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
|
||||
assertTrue(atStatesDataList.isEmpty());
|
||||
}
|
||||
|
||||
// ... but blocks 6-10 have block data and full AT states data
|
||||
for (Integer i=6; i <= 10; i++) {
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(i);
|
||||
assertNotNull(blockData.getSignature());
|
||||
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
|
||||
assertNotNull(atStatesDataList);
|
||||
assertFalse(atStatesDataList.isEmpty());
|
||||
ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(atStatesDataList.get(0).getATAddress(), i);
|
||||
assertNotNull(atStatesData.getStateHash());
|
||||
assertNotNull(atStatesData.getStateData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -21,6 +21,7 @@ import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.AtUtils;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
@@ -35,13 +36,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetATStateAtHeightWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
@@ -58,13 +59,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetATStateAtHeightWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
@@ -87,13 +88,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetLatestATStateWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
@@ -111,13 +112,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetLatestATStatePostTrimming() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
@@ -144,14 +145,66 @@ public class AtRepositoryTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
public void testOrphanTrimmedATStates() throws DataException {
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
int maxTrimHeight = blockchainHeight - 4;
|
||||
Integer testHeight = maxTrimHeight + 1;
|
||||
|
||||
// Trim AT state data
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
repository.saveChanges();
|
||||
repository.getATRepository().trimAtStates(2, maxTrimHeight, 1000);
|
||||
|
||||
// Orphan 3 blocks
|
||||
// This leaves one more untrimmed block, so the latest AT state should be available
|
||||
BlockUtils.orphanBlocks(repository, 3);
|
||||
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
assertEquals(testHeight, atStateData.getHeight());
|
||||
|
||||
// We should always have the latest AT state data available
|
||||
assertNotNull(atStateData.getStateData());
|
||||
|
||||
// Orphan 1 more block
|
||||
Exception exception = null;
|
||||
try {
|
||||
BlockUtils.orphanBlocks(repository, 1);
|
||||
} catch (DataException e) {
|
||||
exception = e;
|
||||
}
|
||||
|
||||
// Ensure that a DataException is thrown because there is no more AT states data available
|
||||
assertNotNull(exception);
|
||||
assertEquals(DataException.class, exception.getClass());
|
||||
assertEquals(String.format("Can't find previous AT state data for %s", atAddress), exception.getMessage());
|
||||
|
||||
// FUTURE: we may be able to retain unique AT states when trimming, to avoid this exception
|
||||
// and allow orphaning back through blocks with trimmed AT states.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException {
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
@@ -191,13 +244,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetMatchingFinalATStatesWithDataValue() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
@@ -237,13 +290,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetBlockATStatesAtHeightWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
@@ -264,13 +317,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetBlockATStatesAtHeightWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
|
||||
// Mint a few blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
@@ -297,13 +350,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testSaveATStateWithData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
@@ -328,13 +381,13 @@ public class AtRepositoryTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testSaveATStateWithoutData() throws DataException {
|
||||
byte[] creationBytes = buildSimpleAT();
|
||||
byte[] creationBytes = AtUtils.buildSimpleAT();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
long fundingAmount = 1_00000000L;
|
||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
|
||||
String atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
|
||||
// Mint a few blocks
|
||||
@@ -364,67 +417,4 @@ public class AtRepositoryTests extends Common {
|
||||
assertNull(atStateData.getStateData());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildSimpleAT() {
|
||||
// Pretend we use 4 values in data segment
|
||||
int addrCounter = 4;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
}
|
||||
|
81
src/test/java/org/qortal/test/common/AtUtils.java
Normal file
81
src/test/java/org/qortal/test/common/AtUtils.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package org.qortal.test.common;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class AtUtils {
|
||||
|
||||
public static byte[] buildSimpleAT() {
|
||||
// Pretend we use 4 values in data segment
|
||||
int addrCounter = 4;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
public static DeployAtTransaction doDeployAT(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
}
|
345
src/test/java/org/qortal/test/naming/IntegrityTests.java
Normal file
345
src/test/java/org/qortal/test/naming/IntegrityTests.java
Normal file
@@ -0,0 +1,345 @@
|
||||
package org.qortal.test.naming;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class IntegrityTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Run the database integrity check for this name
|
||||
NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
assertEquals(1, integrityCheck.rebuildName(name, repository));
|
||||
|
||||
// Ensure the name still exists and the data is still correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Now delete the name, to simulate a database inconsistency
|
||||
repository.getNameRepository().delete(name);
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Run the database integrity check for this name and check that a row was modified
|
||||
NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
assertEquals(1, integrityCheck.rebuildName(name, repository));
|
||||
|
||||
// Ensure the name exists again and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingNameAfterUpdate() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Update the name
|
||||
String newData = "{\"age\":31}";
|
||||
UpdateNameTransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, name, newData);
|
||||
TransactionUtils.signAndMint(repository, updateTransactionData, alice);
|
||||
|
||||
// Ensure the name still exists and the data has been updated
|
||||
assertEquals(newData, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Now delete the name, to simulate a database inconsistency
|
||||
repository.getNameRepository().delete(name);
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Run the database integrity check for this name
|
||||
// We expect 2 modifications to be made - the original register name followed by the update
|
||||
NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
assertEquals(2, integrityCheck.rebuildName(name, repository));
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(newData, repository.getNameRepository().fromName(name).getData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingNameAfterRename() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Rename the name
|
||||
String newName = "new-name";
|
||||
String newData = "{\"age\":31}";
|
||||
UpdateNameTransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, newName, newData);
|
||||
TransactionUtils.signAndMint(repository, updateTransactionData, alice);
|
||||
|
||||
// Ensure the new name exists and the data has been updated
|
||||
assertEquals(newData, repository.getNameRepository().fromName(newName).getData());
|
||||
|
||||
// Ensure the old name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Now delete the new name, to simulate a database inconsistency
|
||||
repository.getNameRepository().delete(newName);
|
||||
|
||||
// Ensure the new name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(newName));
|
||||
|
||||
// Attempt to register the new name
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), newName, data);
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
// Transaction should be invalid, because the database inconsistency was fixed by RegisterNameTransaction.preProcess()
|
||||
Transaction.ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be invalid", Transaction.ValidationResult.OK != result);
|
||||
assertTrue("Name should already be registered", Transaction.ValidationResult.NAME_ALREADY_REGISTERED == result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterMissingName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Now delete the name, to simulate a database inconsistency
|
||||
repository.getNameRepository().delete(name);
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Attempt to register the name again
|
||||
String duplicateName = "TEST-nÁme";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
// Transaction should be invalid, because the database inconsistency was fixed by RegisterNameTransaction.preProcess()
|
||||
Transaction.ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be invalid", Transaction.ValidationResult.OK != result);
|
||||
assertTrue("Name should already be registered", Transaction.ValidationResult.NAME_ALREADY_REGISTERED == result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateMissingName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(initialName).getData());
|
||||
|
||||
// Now delete the name, to simulate a database inconsistency
|
||||
repository.getNameRepository().delete(initialName);
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(initialName));
|
||||
|
||||
// Attempt to update the name
|
||||
String newName = "new-name";
|
||||
String newData = "";
|
||||
TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData);
|
||||
Transaction transaction = Transaction.fromData(repository, updateTransactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
// Transaction should be valid, because the database inconsistency was fixed by UpdateNameTransaction.preProcess()
|
||||
Transaction.ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be valid", Transaction.ValidationResult.OK == result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateToMissingName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(initialName).getData());
|
||||
|
||||
// Register the second name that we will ultimately try and rename the first name to
|
||||
String secondName = "new-missing-name";
|
||||
String secondNameData = "{\"data2\":true}";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), secondName, secondNameData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the second name exists and the data is correct
|
||||
assertEquals(secondNameData, repository.getNameRepository().fromName(secondName).getData());
|
||||
|
||||
// Now delete the second name, to simulate a database inconsistency
|
||||
repository.getNameRepository().delete(secondName);
|
||||
|
||||
// Ensure the second name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(secondName));
|
||||
|
||||
// Attempt to rename the first name to the second name
|
||||
TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, secondName, secondNameData);
|
||||
Transaction transaction = Transaction.fromData(repository, updateTransactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
// Transaction should be invalid, because the database inconsistency was fixed by UpdateNameTransaction.preProcess()
|
||||
// Therefore the name that we are trying to rename TO already exists
|
||||
Transaction.ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be invalid", Transaction.ValidationResult.OK != result);
|
||||
assertTrue("Destination name should already exist", Transaction.ValidationResult.NAME_ALREADY_REGISTERED == result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSellMissingName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Now delete the name, to simulate a database inconsistency
|
||||
repository.getNameRepository().delete(name);
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Attempt to sell the name
|
||||
TransactionData sellTransactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, 123456);
|
||||
Transaction transaction = Transaction.fromData(repository, sellTransactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
// Transaction should be valid, because the database inconsistency was fixed by SellNameTransaction.preProcess()
|
||||
Transaction.ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be valid", Transaction.ValidationResult.OK == result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuyMissingName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Now delete the name, to simulate a database inconsistency
|
||||
repository.getNameRepository().delete(name);
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Attempt to sell the name
|
||||
long amount = 123456;
|
||||
TransactionData sellTransactionData = new SellNameTransactionData(TestTransaction.generateBase(alice), name, amount);
|
||||
TransactionUtils.signAndMint(repository, sellTransactionData, alice);
|
||||
|
||||
// Ensure the name now exists
|
||||
assertNotNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Now delete the name again, to simulate another database inconsistency
|
||||
repository.getNameRepository().delete(name);
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Bob now attempts to buy the name
|
||||
String seller = alice.getAddress();
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
TransactionData buyTransactionData = new BuyNameTransactionData(TestTransaction.generateBase(bob), name, amount, seller);
|
||||
Transaction transaction = Transaction.fromData(repository, buyTransactionData);
|
||||
transaction.sign(bob);
|
||||
|
||||
// Transaction should be valid, because the database inconsistency was fixed by SellNameTransaction.preProcess()
|
||||
Transaction.ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be valid", Transaction.ValidationResult.OK == result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -7,14 +7,13 @@ import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.data.transaction.RegisterNameTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.UpdateNameTransactionData;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.naming.Name;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.*;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
@@ -32,7 +31,7 @@ public class MiscTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "initial-name";
|
||||
String data = "initial-data";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
@@ -51,7 +50,7 @@ public class MiscTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{}";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
@@ -67,6 +66,30 @@ public class MiscTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
// test trying to register same name twice (with different creator)
|
||||
@Test
|
||||
public void testDuplicateRegisterNameWithDifferentCreator() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// duplicate (this time registered by Bob)
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
String duplicateName = "TEST-nÁme";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), duplicateName, data);
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be invalid", ValidationResult.OK != result);
|
||||
}
|
||||
}
|
||||
|
||||
// test register then trying to update another name to existing name
|
||||
@Test
|
||||
public void testUpdateToExistingName() throws DataException {
|
||||
@@ -74,7 +97,7 @@ public class MiscTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{}";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
@@ -103,7 +126,7 @@ public class MiscTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = alice.getAddress();
|
||||
String data = "{}";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
@@ -121,7 +144,7 @@ public class MiscTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{}";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
@@ -138,4 +161,147 @@ public class MiscTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
// test registering and then orphaning
|
||||
@Test
|
||||
public void testRegisterNameAndOrphan() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Register the name
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Orphan the latest block
|
||||
BlockUtils.orphanBlocks(repository, 1);
|
||||
|
||||
// Ensure the name doesn't exist once again
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrphanAndReregisterName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Register the name
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// Orphan the latest block
|
||||
BlockUtils.orphanBlocks(repository, 1);
|
||||
|
||||
// Ensure the name doesn't exist once again
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Now check there is an unconfirmed transaction
|
||||
assertEquals(1, repository.getTransactionRepository().getUnconfirmedTransactions().size());
|
||||
|
||||
// Re-mint the block, including the original transaction
|
||||
BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
|
||||
// There should no longer be an unconfirmed transaction
|
||||
assertEquals(0, repository.getTransactionRepository().getUnconfirmedTransactions().size());
|
||||
|
||||
// Orphan the latest block
|
||||
BlockUtils.orphanBlocks(repository, 1);
|
||||
|
||||
// There should now be an unconfirmed transaction again
|
||||
assertEquals(1, repository.getTransactionRepository().getUnconfirmedTransactions().size());
|
||||
|
||||
// Re-mint the block, including the original transaction
|
||||
BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
|
||||
// Ensure there are no unconfirmed transactions
|
||||
assertEquals(0, repository.getTransactionRepository().getUnconfirmedTransactions().size());
|
||||
}
|
||||
}
|
||||
|
||||
// test registering and then orphaning multiple times, with a different versions of the transaction each time
|
||||
// we can sometimes end up with more than one version of a transaction, if it is signed and submitted twice
|
||||
@Test
|
||||
public void testMultipleRegisterNameAndOrphan() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Register the name
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
assertEquals(data, repository.getNameRepository().fromName(name).getData());
|
||||
|
||||
// The number of unconfirmed transactions should equal the number of cycles minus 1 (because one is in a block)
|
||||
// If more than one made it into a block, this test would fail
|
||||
assertEquals(i-1, repository.getTransactionRepository().getUnconfirmedTransactions().size());
|
||||
|
||||
// Orphan the latest block
|
||||
BlockUtils.orphanBlocks(repository, 1);
|
||||
|
||||
// The number of unconfirmed transactions should equal the number of cycles
|
||||
assertEquals(i, repository.getTransactionRepository().getUnconfirmedTransactions().size());
|
||||
|
||||
// Ensure the name doesn't exist once again
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (int i=0; i<10; i++) {
|
||||
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Register the name
|
||||
Name nameObj = new Name(repository, transactionData);
|
||||
nameObj.register();
|
||||
|
||||
// Ensure the name now exists
|
||||
assertNotNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
// Unregister the name
|
||||
nameObj.unregister();
|
||||
|
||||
// Ensure the name doesn't exist again
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import static org.junit.Assert.*;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.RegisterNameTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.UpdateNameTransactionData;
|
||||
@@ -29,12 +30,21 @@ public class UpdateTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "initial-name";
|
||||
String initialData = "initial-data";
|
||||
String initialReducedName = "initia1-name";
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||
|
||||
// Check name, reduced name, and data exist
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
NameData nameData = repository.getNameRepository().fromName(initialName);
|
||||
assertEquals("initia1-name", nameData.getReducedName());
|
||||
assertEquals(initialData, nameData.getData());
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
String newName = "new-name";
|
||||
String newReducedName = "new-name";
|
||||
String newData = "";
|
||||
TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData);
|
||||
TransactionUtils.signAndMint(repository, updateTransactionData, alice);
|
||||
@@ -42,20 +52,37 @@ public class UpdateTests extends Common {
|
||||
// Check old name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(initialName));
|
||||
|
||||
// Check reduced name no longer exists
|
||||
assertNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check new name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(newName));
|
||||
|
||||
// Check reduced name and data are correct for new name
|
||||
NameData newNameData = repository.getNameRepository().fromName(newReducedName);
|
||||
assertEquals(newReducedName, newNameData.getReducedName());
|
||||
// Data should remain the same because it was empty in the UpdateNameTransactionData
|
||||
assertEquals(initialData, newNameData.getData());
|
||||
|
||||
// Check updated timestamp is correct
|
||||
assertEquals((Long) updateTransactionData.getTimestamp(), repository.getNameRepository().fromName(newName).getUpdated());
|
||||
|
||||
// orphan and recheck
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check new name no longer exists
|
||||
// Check new name and reduced name no longer exist
|
||||
assertFalse(repository.getNameRepository().nameExists(newName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(newReducedName));
|
||||
|
||||
// Check old name exists again
|
||||
// Check old name and reduced name exist again
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check data and reduced name are still present for this name
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
nameData = repository.getNameRepository().fromName(initialName);
|
||||
assertEquals(initialReducedName, nameData.getReducedName());
|
||||
assertEquals(initialData, nameData.getData());
|
||||
|
||||
// Check updated timestamp is empty
|
||||
assertNull(repository.getNameRepository().fromName(initialName).getUpdated());
|
||||
@@ -68,11 +95,17 @@ public class UpdateTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "initial-name";
|
||||
String initialData = "initial-data";
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
String constantReducedName = "initia1-name";
|
||||
|
||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(constantReducedName));
|
||||
|
||||
String newName = "Initial-Name";
|
||||
String newData = "";
|
||||
TransactionData updateTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData);
|
||||
@@ -83,6 +116,7 @@ public class UpdateTests extends Common {
|
||||
|
||||
// Check new name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(newName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(constantReducedName));
|
||||
|
||||
// Check updated timestamp is correct
|
||||
assertEquals((Long) updateTransactionData.getTimestamp(), repository.getNameRepository().fromName(newName).getUpdated());
|
||||
@@ -95,6 +129,7 @@ public class UpdateTests extends Common {
|
||||
|
||||
// Check old name exists again
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(constantReducedName));
|
||||
|
||||
// Check updated timestamp is empty
|
||||
assertNull(repository.getNameRepository().fromName(initialName).getUpdated());
|
||||
@@ -108,32 +143,43 @@ public class UpdateTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "initial-name";
|
||||
String initialData = "initial-data";
|
||||
String initialReducedName = "initia1-name";
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
String middleName = "middle-name";
|
||||
String middleReducedName = "midd1e-name";
|
||||
String middleData = "";
|
||||
TransactionData middleTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData);
|
||||
TransactionUtils.signAndMint(repository, middleTransactionData, alice);
|
||||
|
||||
// Check old name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(initialName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check new name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(middleName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(middleReducedName));
|
||||
|
||||
String newestName = "newest-name";
|
||||
String newestReducedName = "newest-name";
|
||||
String newestData = "newest-data";
|
||||
TransactionData newestTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData);
|
||||
TransactionUtils.signAndMint(repository, newestTransactionData, alice);
|
||||
|
||||
// Check previous name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(middleName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(middleReducedName));
|
||||
|
||||
// Check newest name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(newestName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(newestReducedName));
|
||||
|
||||
// Check updated timestamp is correct
|
||||
assertEquals((Long) newestTransactionData.getTimestamp(), repository.getNameRepository().fromName(newestName).getUpdated());
|
||||
@@ -143,9 +189,11 @@ public class UpdateTests extends Common {
|
||||
|
||||
// Check newest name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(newestName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(newestReducedName));
|
||||
|
||||
// Check previous name exists again
|
||||
assertTrue(repository.getNameRepository().nameExists(middleName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(middleReducedName));
|
||||
|
||||
// Check updated timestamp is correct
|
||||
assertEquals((Long) middleTransactionData.getTimestamp(), repository.getNameRepository().fromName(middleName).getUpdated());
|
||||
@@ -155,9 +203,11 @@ public class UpdateTests extends Common {
|
||||
|
||||
// Check new name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(middleName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(middleReducedName));
|
||||
|
||||
// Check original name exists again
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check updated timestamp is empty
|
||||
assertNull(repository.getNameRepository().fromName(initialName).getUpdated());
|
||||
@@ -171,11 +221,16 @@ public class UpdateTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "initial-name";
|
||||
String initialData = "initial-data";
|
||||
String initialReducedName = "initia1-name";
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Don't update name, but update data.
|
||||
// This tests whether reverting a future update/sale can find the correct previous name
|
||||
String middleName = "";
|
||||
@@ -185,29 +240,35 @@ public class UpdateTests extends Common {
|
||||
|
||||
// Check old name still exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
String newestName = "newest-name";
|
||||
String newestReducedName = "newest-name";
|
||||
String newestData = "newest-data";
|
||||
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newestName, newestData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check previous name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(initialName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check newest name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(newestName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(newestReducedName));
|
||||
|
||||
// orphan and recheck
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check original name exists again
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// orphan and recheck
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check original name still exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,11 +278,16 @@ public class UpdateTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "initial-name";
|
||||
String initialData = "initial-data";
|
||||
String initialReducedName = "initia1-name";
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
String newName = "";
|
||||
String newData = "new-data";
|
||||
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, newName, newData);
|
||||
@@ -229,6 +295,7 @@ public class UpdateTests extends Common {
|
||||
|
||||
// Check name still exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check data is correct
|
||||
assertEquals(newData, repository.getNameRepository().fromName(initialName).getData());
|
||||
@@ -238,6 +305,7 @@ public class UpdateTests extends Common {
|
||||
|
||||
// Check name still exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check old data restored
|
||||
assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData());
|
||||
@@ -251,13 +319,19 @@ public class UpdateTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "initial-name";
|
||||
String initialData = "initial-data";
|
||||
String initialReducedName = "initia1-name";
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Update data
|
||||
String middleName = "middle-name";
|
||||
String middleReducedName = "midd1e-name";
|
||||
String middleData = "middle-data";
|
||||
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
@@ -266,6 +340,7 @@ public class UpdateTests extends Common {
|
||||
assertEquals(middleData, repository.getNameRepository().fromName(middleName).getData());
|
||||
|
||||
String newestName = "newest-name";
|
||||
String newestReducedName = "newest-name";
|
||||
String newestData = "newest-data";
|
||||
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
@@ -273,6 +348,14 @@ public class UpdateTests extends Common {
|
||||
// Check data is correct
|
||||
assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData());
|
||||
|
||||
// Check initial name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(initialName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check newest name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(newestName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(newestReducedName));
|
||||
|
||||
// orphan and recheck
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
@@ -284,6 +367,10 @@ public class UpdateTests extends Common {
|
||||
|
||||
// Check data is correct
|
||||
assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData());
|
||||
|
||||
// Check initial name exists again
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,38 +381,69 @@ public class UpdateTests extends Common {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String initialName = "initial-name";
|
||||
String initialData = "initial-data";
|
||||
String initialReducedName = "initia1-name";
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Don't update data, but update name.
|
||||
// This tests whether reverting a future update/sale can find the correct previous data
|
||||
String middleName = "middle-name";
|
||||
String middleReducedName = "midd1e-name";
|
||||
String middleData = "";
|
||||
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), initialName, middleName, middleData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check original name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(initialName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check middle name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(middleName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(middleReducedName));
|
||||
|
||||
// Check data is correct
|
||||
assertEquals(initialData, repository.getNameRepository().fromName(middleName).getData());
|
||||
|
||||
String newestName = "newest-name";
|
||||
String newestReducedName = "newest-name";
|
||||
String newestData = "newest-data";
|
||||
transactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), middleName, newestName, newestData);
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check middle name no longer exists
|
||||
assertFalse(repository.getNameRepository().nameExists(middleName));
|
||||
assertNull(repository.getNameRepository().fromReducedName(middleReducedName));
|
||||
|
||||
// Check newest name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(newestName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(newestReducedName));
|
||||
|
||||
// Check data is correct
|
||||
assertEquals(newestData, repository.getNameRepository().fromName(newestName).getData());
|
||||
|
||||
// orphan and recheck
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check middle name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(middleName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(middleReducedName));
|
||||
|
||||
// Check data is correct
|
||||
assertEquals(initialData, repository.getNameRepository().fromName(middleName).getData());
|
||||
|
||||
// orphan and recheck
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check initial name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(initialReducedName));
|
||||
|
||||
// Check data is correct
|
||||
assertEquals(initialData, repository.getNameRepository().fromName(initialName).getData());
|
||||
}
|
||||
|
11
src/test/resources/test-settings-v2-block-archive.json
Normal file
11
src/test/resources/test-settings-v2-block-archive.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"bitcoinNet": "TEST3",
|
||||
"litecoinNet": "TEST3",
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0,
|
||||
"pruneBlockLimit": 1450,
|
||||
"repositoryPath": "dbtest"
|
||||
}
|
Reference in New Issue
Block a user