mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-30 05:31:23 +00:00
Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8331241d75 | ||
|
e041748b48 | ||
|
06691af729 | ||
|
735de93848 | ||
|
57485bfe36 | ||
|
23ec71d7be | ||
|
5bbde4dcdb | ||
|
dc2da8b283 | ||
|
f3772d19f5 | ||
|
b08329dcf1 | ||
|
668be633c4 | ||
|
73a7c1fe7e | ||
|
f9f34a61ac | ||
|
a555f503eb | ||
|
9968865d0e | ||
|
05eb337367 | ||
|
5386db8a3f | ||
|
edae7fd844 | ||
|
4840804d32 | ||
|
0388626e42 | ||
|
c5c0dcf0f2 | ||
|
384f592f59 | ||
|
b4a736c5d2 | ||
|
4afbca7ed2 | ||
|
b1452bddf3 | ||
|
96ac883515 | ||
|
b6803490b9 | ||
|
3739920ad3 | ||
|
7f21ea7e00 | ||
|
7d7cea3278 | ||
|
0b05de22a0 | ||
|
abdc265fc6 | ||
|
1153519d78 | ||
|
0af6fbe1eb | ||
|
d54006caf7 | ||
|
e1771dbaea | ||
|
cc98abeffb | ||
|
a3702ac6b0 | ||
|
69902f7f5b | ||
|
999e8b8aca | ||
|
466c727dee | ||
|
ba9f3b335c | ||
|
148ca0af05 | ||
|
c39b9c764b | ||
|
d30eb6141a | ||
|
76f17dda53 | ||
|
ae5b713e58 | ||
|
257ca2da05 | ||
|
d27316eb64 | ||
|
64d8353629 | ||
|
1abceada20 | ||
|
4c463f65b7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,7 +14,6 @@
|
||||
/.mvn.classpath
|
||||
/notes*
|
||||
/settings.json
|
||||
/testnet*
|
||||
/settings*.json
|
||||
/testchain*.json
|
||||
/run-testnet*.sh
|
||||
|
2
pom.xml
2
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.8.7</version>
|
||||
<version>3.9.1</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
|
@@ -79,7 +79,7 @@ public enum ApiError {
|
||||
// BUYER_ALREADY_OWNER(411, 422),
|
||||
|
||||
// POLLS
|
||||
// POLL_NO_EXISTS(501, 404),
|
||||
POLL_NO_EXISTS(501, 404),
|
||||
// POLL_ALREADY_EXISTS(502, 422),
|
||||
// DUPLICATE_OPTION(503, 422),
|
||||
// POLL_OPTION_NO_EXISTS(504, 404),
|
||||
|
@@ -45,6 +45,7 @@ import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.controller.repository.BlockArchiveRebuilder;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.network.Network;
|
||||
@@ -152,6 +153,22 @@ public class AdminResource {
|
||||
return nodeStatus;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/settings")
|
||||
@Operation(
|
||||
summary = "Fetch node settings",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Settings.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
public Settings settings() {
|
||||
Settings nodeSettings = Settings.getInstance();
|
||||
|
||||
return nodeSettings;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stop")
|
||||
@Operation(
|
||||
@@ -734,6 +751,64 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/archive/rebuild")
|
||||
@Operation(
|
||||
summary = "Rebuild archive",
|
||||
description = "Rebuilds archive files, using the specified serialization version",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number", example = "2"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String rebuildArchive(@HeaderParam(Security.API_KEY_HEADER) String apiKey, Integer serializationVersion) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
// Default serialization version to value specified in settings
|
||||
if (serializationVersion == null) {
|
||||
serializationVersion = Settings.getInstance().getDefaultArchiveVersion();
|
||||
}
|
||||
|
||||
try {
|
||||
// We don't actually need to lock the blockchain here, but we'll do it anyway so that
|
||||
// the node can focus on rebuilding rather than synchronizing / minting.
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
BlockArchiveRebuilder blockArchiveRebuilder = new BlockArchiveRebuilder(serializationVersion);
|
||||
blockArchiveRebuilder.start();
|
||||
|
||||
return "true";
|
||||
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform rebuild
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/repository")
|
||||
@Operation(
|
||||
|
@@ -501,6 +501,9 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
for (ArbitraryTransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getService() == null) {
|
||||
continue;
|
||||
}
|
||||
ArbitraryResourceInfo arbitraryResourceInfo = new ArbitraryResourceInfo();
|
||||
arbitraryResourceInfo.name = transactionData.getName();
|
||||
arbitraryResourceInfo.service = transactionData.getService();
|
||||
@@ -773,6 +776,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
String path) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -781,7 +785,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, path, null, null, false,
|
||||
title, description, tags, category);
|
||||
fee, title, description, tags, category);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -818,6 +822,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
String path) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -826,7 +831,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, path, null, null, false,
|
||||
title, description, tags, category);
|
||||
fee, title, description, tags, category);
|
||||
}
|
||||
|
||||
|
||||
@@ -864,6 +869,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
String base64) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -872,7 +878,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64, false,
|
||||
title, description, tags, category);
|
||||
fee, title, description, tags, category);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -907,6 +913,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
String base64) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -915,7 +922,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64, false,
|
||||
title, description, tags, category);
|
||||
fee, title, description, tags, category);
|
||||
}
|
||||
|
||||
|
||||
@@ -952,6 +959,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
String base64Zip) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -960,7 +968,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64Zip, true,
|
||||
title, description, tags, category);
|
||||
fee, title, description, tags, category);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -995,6 +1003,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
String base64Zip) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1003,7 +1012,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64Zip, true,
|
||||
title, description, tags, category);
|
||||
fee, title, description, tags, category);
|
||||
}
|
||||
|
||||
|
||||
@@ -1043,6 +1052,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
String string) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1051,7 +1061,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, string, null, false,
|
||||
title, description, tags, category);
|
||||
fee, title, description, tags, category);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -1088,6 +1098,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
String string) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1096,14 +1107,14 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, string, null, false,
|
||||
title, description, tags, category);
|
||||
fee, title, description, tags, category);
|
||||
}
|
||||
|
||||
|
||||
// Shared methods
|
||||
|
||||
private String upload(Service service, String name, String identifier,
|
||||
String path, String string, String base64, boolean zipped,
|
||||
private String upload(Service service, String name, String identifier, String path,
|
||||
String string, String base64, boolean zipped, Long fee,
|
||||
String title, String description, List<String> tags, Category category) {
|
||||
// Fetch public key from registered name
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -1167,9 +1178,14 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
// Default to zero fee if not specified
|
||||
if (fee == null) {
|
||||
fee = 0L;
|
||||
}
|
||||
|
||||
try {
|
||||
ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder(
|
||||
repository, publicKey58, Paths.get(path), name, null, service, identifier,
|
||||
repository, publicKey58, fee, Paths.get(path), name, null, service, identifier,
|
||||
title, description, tags, category
|
||||
);
|
||||
|
||||
|
@@ -48,6 +48,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
@Path("/blocks")
|
||||
@Tag(name = "Blocks")
|
||||
@@ -165,10 +166,13 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
// Not found, so try the block archive
|
||||
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
|
||||
if (bytes != null) {
|
||||
if (version != 1) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Archived blocks require version 1");
|
||||
Triple<byte[], Integer, Integer> serializedBlock = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
|
||||
if (serializedBlock != null) {
|
||||
byte[] bytes = serializedBlock.getA();
|
||||
Integer serializationVersion = serializedBlock.getB();
|
||||
if (version != serializationVersion) {
|
||||
// TODO: we could quite easily reserialize the block with the requested version
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Block is not stored using requested serialization version.");
|
||||
}
|
||||
return Base58.encode(bytes);
|
||||
}
|
||||
|
@@ -40,6 +40,8 @@ import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@Path("/chat")
|
||||
@Tag(name = "Chat")
|
||||
public class ChatResource {
|
||||
@@ -72,6 +74,8 @@ public class ChatResource {
|
||||
@QueryParam("reference") String reference,
|
||||
@QueryParam("chatreference") String chatReference,
|
||||
@QueryParam("haschatreference") Boolean hasChatReference,
|
||||
@QueryParam("sender") String sender,
|
||||
@QueryParam("encoding") Encoding encoding,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
@@ -107,6 +111,8 @@ public class ChatResource {
|
||||
chatReferenceBytes,
|
||||
hasChatReference,
|
||||
involvingAddresses,
|
||||
sender,
|
||||
encoding,
|
||||
limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
@@ -129,7 +135,7 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public ChatMessage getMessageBySignature(@PathParam("signature") String signature58) {
|
||||
public ChatMessage getMessageBySignature(@PathParam("signature") String signature58, @QueryParam("encoding") Encoding encoding) {
|
||||
byte[] signature = Base58.decode(signature58);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -139,7 +145,7 @@ public class ChatResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Message not found");
|
||||
}
|
||||
|
||||
return repository.getChatRepository().toChatMessage(chatTransactionData);
|
||||
return repository.getChatRepository().toChatMessage(chatTransactionData, encoding);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -162,12 +168,12 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public ActiveChats getActiveChats(@PathParam("address") String address) {
|
||||
public ActiveChats getActiveChats(@PathParam("address") String address, @QueryParam("encoding") Encoding encoding) {
|
||||
if (address == null || !Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getChatRepository().getActiveChats(address);
|
||||
return repository.getChatRepository().getActiveChats(address, encoding);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
197
src/main/java/org/qortal/api/resource/PollsResource.java
Normal file
197
src/main/java/org/qortal/api/resource/PollsResource.java
Normal file
@@ -0,0 +1,197 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.data.transaction.CreatePollTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.VoteOnPollTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.CreatePollTransactionTransformer;
|
||||
import org.qortal.transform.transaction.PaymentTransactionTransformer;
|
||||
import org.qortal.transform.transaction.VoteOnPollTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import java.util.List;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.data.voting.PollData;
|
||||
|
||||
@Path("/polls")
|
||||
@Tag(name = "Polls")
|
||||
public class PollsResource {
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Operation(
|
||||
summary = "List all polls",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "poll info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(schema = @Schema(implementation = PollData.class))
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<PollData> getAllPolls(@Parameter(
|
||||
ref = "limit"
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
) @QueryParam("offset") Integer offset, @Parameter(
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<PollData> allPollData = repository.getVotingRepository().getAllPolls(limit, offset, reverse);
|
||||
return allPollData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{pollName}")
|
||||
@Operation(
|
||||
summary = "Info on poll",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "poll info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = PollData.class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public PollData getPollData(@PathParam("pollName") String pollName) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PollData pollData = repository.getVotingRepository().fromPollName(pollName);
|
||||
if (pollData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.POLL_NO_EXISTS);
|
||||
|
||||
return pollData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/create")
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, CREATE_POLL transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CreatePollTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, CREATE_POLL transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String CreatePoll(CreatePollTransactionData transactionData) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
Transaction.ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != Transaction.ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = CreatePollTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/vote")
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, VOTE_ON_POLL transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = VoteOnPollTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, VOTE_ON_POLL transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String VoteOnPoll(VoteOnPollTransactionData transactionData) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
Transaction.ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != Transaction.ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = VoteOnPollTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -2,7 +2,9 @@ package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
@@ -21,6 +23,8 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
@@ -62,7 +66,9 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
if (Objects.equals(message, "ping")) {
|
||||
session.getRemote().sendStringByFuture("pong");
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {
|
||||
@@ -75,7 +81,7 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress);
|
||||
ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress, getTargetEncoding(session));
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
@@ -93,4 +99,12 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
private Encoding getTargetEncoding(Session session) {
|
||||
// Default to Base58 if not specified, for backwards support
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
List<String> encodingList = queryParams.get("encoding");
|
||||
String encoding = (encodingList != null && encodingList.size() == 1) ? encodingList.get(0) : "BASE58";
|
||||
return Encoding.valueOf(encoding);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -2,10 +2,7 @@ package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
@@ -22,6 +19,8 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
@@ -35,6 +34,16 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
Encoding encoding = getTargetEncoding(session);
|
||||
|
||||
List<String> limitList = queryParams.get("limit");
|
||||
Integer limit = (limitList != null && limitList.size() == 1) ? Integer.parseInt(limitList.get(0)) : null;
|
||||
|
||||
List<String> offsetList = queryParams.get("offset");
|
||||
Integer offset = (offsetList != null && offsetList.size() == 1) ? Integer.parseInt(offsetList.get(0)) : null;
|
||||
|
||||
List<String> reverseList = queryParams.get("offset");
|
||||
Boolean reverse = (reverseList != null && reverseList.size() == 1) ? Boolean.getBoolean(reverseList.get(0)) : null;
|
||||
|
||||
List<String> txGroupIds = queryParams.get("txGroupId");
|
||||
if (txGroupIds != null && txGroupIds.size() == 1) {
|
||||
@@ -49,7 +58,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, null, null);
|
||||
null,
|
||||
encoding,
|
||||
limit, offset, reverse);
|
||||
|
||||
sendMessages(session, chatMessages);
|
||||
} catch (DataException e) {
|
||||
@@ -79,7 +90,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
null,
|
||||
null,
|
||||
involvingAddresses,
|
||||
null, null, null);
|
||||
null,
|
||||
encoding,
|
||||
limit, offset, reverse);
|
||||
|
||||
sendMessages(session, chatMessages);
|
||||
} catch (DataException e) {
|
||||
@@ -105,7 +118,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
if (Objects.equals(message, "ping")) {
|
||||
session.getRemote().sendStringByFuture("pong");
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {
|
||||
@@ -153,7 +168,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
// Convert ChatTransactionData to ChatMessage
|
||||
ChatMessage chatMessage;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
chatMessage = repository.getChatRepository().toChatMessage(chatTransactionData);
|
||||
chatMessage = repository.getChatRepository().toChatMessage(chatTransactionData, getTargetEncoding(session));
|
||||
} catch (DataException e) {
|
||||
// No output this time?
|
||||
return;
|
||||
@@ -162,4 +177,12 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
sendMessages(session, Collections.singletonList(chatMessage));
|
||||
}
|
||||
|
||||
private Encoding getTargetEncoding(Session session) {
|
||||
// Default to Base58 if not specified, for backwards support
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
List<String> encodingList = queryParams.get("encoding");
|
||||
String encoding = (encodingList != null && encodingList.size() == 1) ? encodingList.get(0) : "BASE58";
|
||||
return Encoding.valueOf(encoding);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -46,6 +46,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
private static final double MAX_FILE_DIFF = 0.5f;
|
||||
|
||||
private final String publicKey58;
|
||||
private final long fee;
|
||||
private final Path path;
|
||||
private final String name;
|
||||
private Method method;
|
||||
@@ -64,11 +65,12 @@ public class ArbitraryDataTransactionBuilder {
|
||||
private ArbitraryTransactionData arbitraryTransactionData;
|
||||
private ArbitraryDataFile arbitraryDataFile;
|
||||
|
||||
public ArbitraryDataTransactionBuilder(Repository repository, String publicKey58, Path path, String name,
|
||||
public ArbitraryDataTransactionBuilder(Repository repository, String publicKey58, long fee, Path path, String name,
|
||||
Method method, Service service, String identifier,
|
||||
String title, String description, List<String> tags, Category category) {
|
||||
this.repository = repository;
|
||||
this.publicKey58 = publicKey58;
|
||||
this.fee = fee;
|
||||
this.path = path;
|
||||
this.name = name;
|
||||
this.method = method;
|
||||
@@ -261,7 +263,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
}
|
||||
|
||||
final BaseTransactionData baseTransactionData = new BaseTransactionData(now, Group.NO_GROUP,
|
||||
lastReference, creatorPublicKey, 0L, null);
|
||||
lastReference, creatorPublicKey, fee, null);
|
||||
final int size = (int) arbitraryDataFile.size();
|
||||
final int version = 5;
|
||||
final int nonce = 0;
|
||||
@@ -272,7 +274,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
final List<PaymentData> payments = new ArrayList<>();
|
||||
|
||||
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
|
||||
version, service, nonce, size, name, identifier, method,
|
||||
version, service.value, nonce, size, name, identifier, method,
|
||||
secret, compression, digest, dataType, metadataHash, payments);
|
||||
|
||||
this.arbitraryTransactionData = transactionData;
|
||||
|
@@ -657,6 +657,10 @@ public class Block {
|
||||
return this.atStates;
|
||||
}
|
||||
|
||||
public byte[] getAtStatesHash() {
|
||||
return this.atStatesHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return expanded info on block's online accounts.
|
||||
* <p>
|
||||
|
@@ -78,7 +78,8 @@ public class BlockChain {
|
||||
onlineAccountMinterLevelValidationHeight,
|
||||
selfSponsorshipAlgoV1Height,
|
||||
feeValidationFixTimestamp,
|
||||
chatReferenceTimestamp;
|
||||
chatReferenceTimestamp,
|
||||
arbitraryOptionalFeeTimestamp;
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@@ -522,6 +523,10 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getArbitraryOptionalFeeTimestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.arbitraryOptionalFeeTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
|
||||
// More complex getters for aspects that change by height or timestamp
|
||||
|
||||
|
@@ -400,12 +400,8 @@ public class Controller extends Thread {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getRepositoryUrl());
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
RepositoryManager.archive(repository);
|
||||
RepositoryManager.prune(repository);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
}
|
||||
catch (DataException e) {
|
||||
// If exception has no cause then repository is in use by some other process.
|
||||
if (e.getCause() == null) {
|
||||
LOGGER.info("Repository in use by another process?");
|
||||
@@ -1379,9 +1375,24 @@ 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, true, repository);
|
||||
if (bytes != null) {
|
||||
CachedBlockMessage blockMessage = new CachedBlockMessage(bytes);
|
||||
Triple<byte[], Integer, Integer> serializedBlock = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, true, repository);
|
||||
if (serializedBlock != null) {
|
||||
byte[] bytes = serializedBlock.getA();
|
||||
Integer serializationVersion = serializedBlock.getB();
|
||||
|
||||
Message blockMessage;
|
||||
switch (serializationVersion) {
|
||||
case 1:
|
||||
blockMessage = new CachedBlockMessage(bytes);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
blockMessage = new CachedBlockV2Message(bytes);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
blockMessage.setId(message.getId());
|
||||
|
||||
// This call also causes the other needed data to be pulled in from repository
|
||||
|
@@ -163,7 +163,7 @@ public class PirateChainWalletController extends Thread {
|
||||
|
||||
// Library not found, so check if we've fetched the resource from QDN
|
||||
ArbitraryTransactionData t = this.getTransactionData(repository);
|
||||
if (t == null) {
|
||||
if (t == null || t.getService() == null) {
|
||||
// Can't find the transaction - maybe on a different chain?
|
||||
return;
|
||||
}
|
||||
|
@@ -137,7 +137,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
// Fetch the transaction data
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
if (arbitraryTransactionData == null) {
|
||||
if (arbitraryTransactionData == null || arbitraryTransactionData.getService() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
if (completeFileExists && !allChunksExist) {
|
||||
// We have the complete file but not the chunks, so let's convert it
|
||||
LOGGER.info(String.format("Transaction %s has complete file but no chunks",
|
||||
LOGGER.debug(String.format("Transaction %s has complete file but no chunks",
|
||||
Base58.encode(arbitraryTransactionData.getSignature())));
|
||||
|
||||
ArbitraryTransactionUtils.convertFileToChunks(arbitraryTransactionData, now, STALE_FILE_TIMEOUT);
|
||||
|
@@ -398,6 +398,11 @@ public class ArbitraryDataManager extends Thread {
|
||||
// Entrypoint to request new metadata from peers
|
||||
public ArbitraryDataTransactionMetadata fetchMetadata(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
|
||||
if (arbitraryTransactionData.getService() == null) {
|
||||
// Can't fetch metadata without a valid service
|
||||
return null;
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(
|
||||
arbitraryTransactionData.getName(),
|
||||
ArbitraryDataFile.ResourceIdType.NAME,
|
||||
@@ -489,7 +494,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
public void invalidateCache(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
String signature58 = Base58.encode(arbitraryTransactionData.getSignature());
|
||||
|
||||
if (arbitraryTransactionData.getName() != null) {
|
||||
if (arbitraryTransactionData.getName() != null && arbitraryTransactionData.getService() != null) {
|
||||
String resourceId = arbitraryTransactionData.getName().toLowerCase();
|
||||
Service service = arbitraryTransactionData.getService();
|
||||
String identifier = arbitraryTransactionData.getIdentifier();
|
||||
|
@@ -0,0 +1,121 @@
|
||||
package org.qortal.controller.repository;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
|
||||
public class BlockArchiveRebuilder {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiveRebuilder.class);
|
||||
|
||||
private final int serializationVersion;
|
||||
|
||||
public BlockArchiveRebuilder(int serializationVersion) {
|
||||
this.serializationVersion = serializationVersion;
|
||||
}
|
||||
|
||||
public void start() throws DataException, IOException {
|
||||
if (!Settings.getInstance().isArchiveEnabled() || Settings.getInstance().isLite()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// New archive path is in a different location from original archive path, to avoid conflicts.
|
||||
// It will be moved later, once the process is complete.
|
||||
final Path newArchivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive-rebuild");
|
||||
final Path originalArchivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive");
|
||||
|
||||
// Delete archive-rebuild if it exists from a previous attempt
|
||||
FileUtils.deleteDirectory(newArchivePath.toFile());
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int startHeight = 1; // We need to rebuild the entire archive
|
||||
|
||||
LOGGER.info("Rebuilding block archive from height {}...", startHeight);
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
Thread.sleep(1000L);
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rebuild archive
|
||||
try {
|
||||
final int maximumArchiveHeight = BlockArchiveReader.getInstance().getHeightOfLastArchivedBlock();
|
||||
if (startHeight >= maximumArchiveHeight) {
|
||||
// We've finished.
|
||||
// Delete existing archive and move the newly built one into its place
|
||||
FileUtils.deleteDirectory(originalArchivePath.toFile());
|
||||
FileUtils.moveDirectory(newArchivePath.toFile(), originalArchivePath.toFile());
|
||||
BlockArchiveReader.getInstance().invalidateFileListCache();
|
||||
LOGGER.info("Block archive successfully rebuilt");
|
||||
return;
|
||||
}
|
||||
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(startHeight, maximumArchiveHeight, serializationVersion, newArchivePath, repository);
|
||||
|
||||
// Set data source to BLOCK_ARCHIVE as we are rebuilding
|
||||
writer.setDataSource(BlockArchiveWriter.BlockArchiveDataSource.BLOCK_ARCHIVE);
|
||||
|
||||
// We can't enforce the 100MB file size target, as the final file needs to contain all blocks
|
||||
// that exist in the current archive. Otherwise, the final blocks in the archive will be lost.
|
||||
writer.setShouldEnforceFileSizeTarget(false);
|
||||
|
||||
// We want to log the rebuild progress
|
||||
writer.setShouldLogProgress(true);
|
||||
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
switch (result) {
|
||||
case OK:
|
||||
// Increment block archive height
|
||||
startHeight += writer.getWrittenCount();
|
||||
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:
|
||||
// This shouldn't happen, as we're not enforcing minimum file sizes
|
||||
repository.discardChanges();
|
||||
throw new DataException("Unable to rebuild archive due to unexpected NOT_ENOUGH_BLOCKS response.");
|
||||
|
||||
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 rebuilding archive. If this error persists, " +
|
||||
"a bootstrap or re-sync may be needed.");
|
||||
repository.discardChanges();
|
||||
throw new DataException("Unable to rebuild archive because a block is missing.");
|
||||
}
|
||||
|
||||
} catch (IOException | TransformationException e) {
|
||||
LOGGER.info("Caught exception when rebuilding block archive", e);
|
||||
throw new DataException("Unable to rebuild block archive");
|
||||
}
|
||||
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Do nothing
|
||||
} finally {
|
||||
// Delete archive-rebuild if it still exists, as that means something went wrong
|
||||
FileUtils.deleteDirectory(newArchivePath.toFile());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -13,7 +13,9 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.Unicode;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class NamesDatabaseIntegrityCheck {
|
||||
|
||||
@@ -28,16 +30,8 @@ public class NamesDatabaseIntegrityCheck {
|
||||
|
||||
private List<TransactionData> nameTransactions = new ArrayList<>();
|
||||
|
||||
|
||||
public int rebuildName(String name, Repository repository) {
|
||||
return this.rebuildName(name, repository, null);
|
||||
}
|
||||
|
||||
public int rebuildName(String name, Repository repository, List<String> referenceNames) {
|
||||
// "referenceNames" tracks the linked names that have already been rebuilt, to prevent circular dependencies
|
||||
if (referenceNames == null) {
|
||||
referenceNames = new ArrayList<>();
|
||||
}
|
||||
|
||||
int modificationCount = 0;
|
||||
try {
|
||||
List<TransactionData> transactions = this.fetchAllTransactionsInvolvingName(name, repository);
|
||||
@@ -46,6 +40,14 @@ public class NamesDatabaseIntegrityCheck {
|
||||
return modificationCount;
|
||||
}
|
||||
|
||||
// If this name has been updated at any point, we need to add transactions from the other names to the sequence
|
||||
int added = this.addAdditionalTransactionsRelatingToName(transactions, name, repository);
|
||||
while (added > 0) {
|
||||
// Keep going until all have been added
|
||||
LOGGER.trace("{} added for {}. Looking for more transactions...", added, name);
|
||||
added = this.addAdditionalTransactionsRelatingToName(transactions, name, repository);
|
||||
}
|
||||
|
||||
// Loop through each past transaction and re-apply it to the Names table
|
||||
for (TransactionData currentTransaction : transactions) {
|
||||
|
||||
@@ -61,29 +63,14 @@ public class NamesDatabaseIntegrityCheck {
|
||||
// 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
|
||||
|
||||
if (!referenceNames.contains(name)) {
|
||||
referenceNames.add(name);
|
||||
this.rebuildName(updateNameTransactionData.getName(), repository, referenceNames);
|
||||
}
|
||||
else {
|
||||
// We've already processed this name so there's nothing more to do
|
||||
}
|
||||
}
|
||||
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()));
|
||||
}
|
||||
Name nameObj = new Name(repository, updateNameTransactionData.getName());
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,8 +341,8 @@ public class NamesDatabaseIntegrityCheck {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by lowest timestamp first
|
||||
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp));
|
||||
// Sort by lowest block height first
|
||||
sortTransactions(transactions);
|
||||
|
||||
return transactions;
|
||||
}
|
||||
@@ -419,4 +406,67 @@ public class NamesDatabaseIntegrityCheck {
|
||||
return names;
|
||||
}
|
||||
|
||||
private int addAdditionalTransactionsRelatingToName(List<TransactionData> transactions, String name, Repository repository) throws DataException {
|
||||
int added = 0;
|
||||
|
||||
// If this name has been updated at any point, we need to add transactions from the other names to the sequence
|
||||
List<String> otherNames = new ArrayList<>();
|
||||
List<TransactionData> updateNameTransactions = transactions.stream().filter(t -> t.getType() == TransactionType.UPDATE_NAME).collect(Collectors.toList());
|
||||
for (TransactionData transactionData : updateNameTransactions) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
||||
// If the newName field isn't empty, and either the "name" or "newName" is different from our reference name,
|
||||
// we should remember this additional name, in case it has relevant transactions associated with it.
|
||||
if (updateNameTransactionData.getNewName() != null && !updateNameTransactionData.getNewName().isEmpty()) {
|
||||
if (!Objects.equals(updateNameTransactionData.getName(), name)) {
|
||||
otherNames.add(updateNameTransactionData.getName());
|
||||
}
|
||||
if (!Objects.equals(updateNameTransactionData.getNewName(), name)) {
|
||||
otherNames.add(updateNameTransactionData.getNewName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (String otherName : otherNames) {
|
||||
List<TransactionData> otherNameTransactions = this.fetchAllTransactionsInvolvingName(otherName, repository);
|
||||
for (TransactionData otherNameTransactionData : otherNameTransactions) {
|
||||
if (!transactions.contains(otherNameTransactionData)) {
|
||||
// Add new transaction relating to other name
|
||||
transactions.add(otherNameTransactionData);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0) {
|
||||
// New transaction(s) added, so re-sort
|
||||
sortTransactions(transactions);
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
private void sortTransactions(List<TransactionData> transactions) {
|
||||
Collections.sort(transactions, new Comparator() {
|
||||
public int compare(Object o1, Object o2) {
|
||||
TransactionData td1 = (TransactionData) o1;
|
||||
TransactionData td2 = (TransactionData) o2;
|
||||
|
||||
// Sort by block height first
|
||||
int heightComparison = td1.getBlockHeight().compareTo(td2.getBlockHeight());
|
||||
if (heightComparison != 0) {
|
||||
return heightComparison;
|
||||
}
|
||||
|
||||
// Same height so compare timestamps
|
||||
int timestampComparison = Long.compare(td1.getTimestamp(), td2.getTimestamp());
|
||||
if (timestampComparison != 0) {
|
||||
return timestampComparison;
|
||||
}
|
||||
|
||||
// Same timestamp so compare signatures
|
||||
return new BigInteger(td1.getSignature()).compareTo(new BigInteger(td2.getSignature()));
|
||||
}});
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,10 +1,15 @@
|
||||
package org.qortal.data.chat;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ActiveChats {
|
||||
|
||||
@@ -18,20 +23,38 @@ public class ActiveChats {
|
||||
private String sender;
|
||||
private String senderName;
|
||||
private byte[] signature;
|
||||
private byte[] data;
|
||||
private Encoding encoding;
|
||||
private String data;
|
||||
|
||||
protected GroupChat() {
|
||||
/* JAXB */
|
||||
}
|
||||
|
||||
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName, byte[] signature, byte[] data) {
|
||||
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName,
|
||||
byte[] signature, Encoding encoding, byte[] data) {
|
||||
this.groupId = groupId;
|
||||
this.groupName = groupName;
|
||||
this.timestamp = timestamp;
|
||||
this.sender = sender;
|
||||
this.senderName = senderName;
|
||||
this.signature = signature;
|
||||
this.data = data;
|
||||
this.encoding = encoding != null ? encoding : Encoding.BASE58;
|
||||
|
||||
if (data != null) {
|
||||
switch (this.encoding) {
|
||||
case BASE64:
|
||||
this.data = Base64.toBase64String(data);
|
||||
break;
|
||||
|
||||
case BASE58:
|
||||
default:
|
||||
this.data = Base58.encode(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.data = null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getGroupId() {
|
||||
@@ -58,7 +81,7 @@ public class ActiveChats {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
public String getData() {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,19 @@
|
||||
package org.qortal.data.chat;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ChatMessage {
|
||||
|
||||
public enum Encoding {
|
||||
BASE58,
|
||||
BASE64
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
private long timestamp;
|
||||
@@ -29,7 +37,9 @@ public class ChatMessage {
|
||||
|
||||
private byte[] chatReference;
|
||||
|
||||
private byte[] data;
|
||||
private Encoding encoding;
|
||||
|
||||
private String data;
|
||||
|
||||
private boolean isText;
|
||||
private boolean isEncrypted;
|
||||
@@ -44,8 +54,8 @@ public class ChatMessage {
|
||||
|
||||
// For repository use
|
||||
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
|
||||
String senderName, String recipient, String recipientName, byte[] chatReference, byte[] data,
|
||||
boolean isText, boolean isEncrypted, byte[] signature) {
|
||||
String senderName, String recipient, String recipientName, byte[] chatReference,
|
||||
Encoding encoding, byte[] data, boolean isText, boolean isEncrypted, byte[] signature) {
|
||||
this.timestamp = timestamp;
|
||||
this.txGroupId = txGroupId;
|
||||
this.reference = reference;
|
||||
@@ -55,7 +65,24 @@ public class ChatMessage {
|
||||
this.recipient = recipient;
|
||||
this.recipientName = recipientName;
|
||||
this.chatReference = chatReference;
|
||||
this.data = data;
|
||||
this.encoding = encoding != null ? encoding : Encoding.BASE58;
|
||||
|
||||
if (data != null) {
|
||||
switch (this.encoding) {
|
||||
case BASE64:
|
||||
this.data = Base64.toBase64String(data);
|
||||
break;
|
||||
|
||||
case BASE58:
|
||||
default:
|
||||
this.data = Base58.encode(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
this.isText = isText;
|
||||
this.isEncrypted = isEncrypted;
|
||||
this.signature = signature;
|
||||
@@ -97,7 +124,7 @@ public class ChatMessage {
|
||||
return this.chatReference;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
public String getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
|
@@ -73,7 +73,7 @@ public class ArbitraryTransactionData extends TransactionData {
|
||||
@Schema(example = "sender_public_key")
|
||||
private byte[] senderPublicKey;
|
||||
|
||||
private Service service;
|
||||
private int service;
|
||||
private int nonce;
|
||||
private int size;
|
||||
|
||||
@@ -103,7 +103,7 @@ public class ArbitraryTransactionData extends TransactionData {
|
||||
}
|
||||
|
||||
public ArbitraryTransactionData(BaseTransactionData baseTransactionData,
|
||||
int version, Service service, int nonce, int size,
|
||||
int version, int service, int nonce, int size,
|
||||
String name, String identifier, Method method, byte[] secret, Compression compression,
|
||||
byte[] data, DataType dataType, byte[] metadataHash, List<PaymentData> payments) {
|
||||
super(TransactionType.ARBITRARY, baseTransactionData);
|
||||
@@ -135,6 +135,10 @@ public class ArbitraryTransactionData extends TransactionData {
|
||||
}
|
||||
|
||||
public Service getService() {
|
||||
return Service.valueOf(this.service);
|
||||
}
|
||||
|
||||
public int getServiceInt() {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
|
@@ -2,9 +2,11 @@ package org.qortal.data.transaction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
|
||||
import org.qortal.data.voting.PollOptionData;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@@ -14,8 +16,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@Schema(allOf = { TransactionData.class })
|
||||
@XmlDiscriminatorValue("CREATE_POLL")
|
||||
public class CreatePollTransactionData extends TransactionData {
|
||||
|
||||
|
||||
@Schema(description = "Poll creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
private byte[] pollCreatorPublicKey;
|
||||
|
||||
// Properties
|
||||
private String owner;
|
||||
private String pollName;
|
||||
@@ -29,10 +36,15 @@ public class CreatePollTransactionData extends TransactionData {
|
||||
super(TransactionType.CREATE_POLL);
|
||||
}
|
||||
|
||||
public void afterUnmarshal(Unmarshaller u, Object parent) {
|
||||
this.creatorPublicKey = this.pollCreatorPublicKey;
|
||||
}
|
||||
|
||||
public CreatePollTransactionData(BaseTransactionData baseTransactionData,
|
||||
String owner, String pollName, String description, List<PollOptionData> pollOptions) {
|
||||
super(Transaction.TransactionType.CREATE_POLL, baseTransactionData);
|
||||
|
||||
this.creatorPublicKey = baseTransactionData.creatorPublicKey;
|
||||
this.owner = owner;
|
||||
this.pollName = pollName;
|
||||
this.description = description;
|
||||
@@ -41,6 +53,7 @@ public class CreatePollTransactionData extends TransactionData {
|
||||
|
||||
// Getters/setters
|
||||
|
||||
public byte[] getPollCreatorPublicKey() { return this.creatorPublicKey; }
|
||||
public String getOwner() {
|
||||
return this.owner;
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.voting.PollData;
|
||||
import org.qortal.transaction.Transaction.ApprovalStatus;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
@@ -29,6 +30,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||
@XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class,
|
||||
SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class,
|
||||
CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class,
|
||||
PollData.class,
|
||||
IssueAssetTransactionData.class, TransferAssetTransactionData.class,
|
||||
CreateAssetOrderTransactionData.class, CancelAssetOrderTransactionData.class,
|
||||
MultiPaymentTransactionData.class, DeployAtTransactionData.class, MessageTransactionData.class, ATTransactionData.class,
|
||||
|
@@ -3,7 +3,9 @@ package org.qortal.data.transaction;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -11,12 +13,17 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@Schema(allOf = { TransactionData.class })
|
||||
@XmlDiscriminatorValue("VOTE_ON_POLL")
|
||||
public class VoteOnPollTransactionData extends TransactionData {
|
||||
|
||||
// Properties
|
||||
@Schema(description = "Vote creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
private byte[] voterPublicKey;
|
||||
private String pollName;
|
||||
private int optionIndex;
|
||||
// For internal use when orphaning
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private Integer previousOptionIndex;
|
||||
|
||||
// Constructors
|
||||
|
@@ -14,6 +14,11 @@ public class PollData {
|
||||
|
||||
// Constructors
|
||||
|
||||
// For JAXB
|
||||
protected PollData() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> pollOptions, long published) {
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
this.owner = owner;
|
||||
@@ -29,22 +34,42 @@ public class PollData {
|
||||
return this.creatorPublicKey;
|
||||
}
|
||||
|
||||
public void setCreatorPublicKey(byte[] creatorPublicKey) {
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return this.owner;
|
||||
}
|
||||
|
||||
public void setOwner(String owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public String getPollName() {
|
||||
return this.pollName;
|
||||
}
|
||||
|
||||
public void setPollName(String pollName) {
|
||||
this.pollName = pollName;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<PollOptionData> getPollOptions() {
|
||||
return this.pollOptions;
|
||||
}
|
||||
|
||||
public void setPollOptions(List<PollOptionData> pollOptions) {
|
||||
this.pollOptions = pollOptions;
|
||||
}
|
||||
|
||||
public long getPublished() {
|
||||
return this.published;
|
||||
}
|
||||
|
@@ -16,6 +16,8 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.Unicode;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Name {
|
||||
|
||||
// Properties
|
||||
@@ -116,7 +118,7 @@ public class Name {
|
||||
|
||||
this.repository.getNameRepository().save(this.nameData);
|
||||
|
||||
if (!updateNameTransactionData.getNewName().isEmpty())
|
||||
if (!updateNameTransactionData.getNewName().isEmpty() && !Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName()))
|
||||
// Name has changed, delete old entry
|
||||
this.repository.getNameRepository().delete(updateNameTransactionData.getNewName());
|
||||
|
||||
|
@@ -0,0 +1,43 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
// This is an OUTGOING-only Message which more readily lends itself to being cached
|
||||
public class CachedBlockV2Message extends Message implements Cloneable {
|
||||
|
||||
public CachedBlockV2Message(Block block) throws TransformationException {
|
||||
super(MessageType.BLOCK_V2);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public CachedBlockV2Message(byte[] cachedBytes) {
|
||||
super(MessageType.BLOCK_V2);
|
||||
|
||||
this.dataBytes = cachedBytes;
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
throw new UnsupportedOperationException("CachedBlockMessageV2 is for outgoing messages only");
|
||||
}
|
||||
|
||||
}
|
@@ -3,10 +3,7 @@ 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.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.BlockTransformation;
|
||||
@@ -67,20 +64,51 @@ public class BlockArchiveReader {
|
||||
this.fileListCache = Map.copyOf(map);
|
||||
}
|
||||
|
||||
public Integer fetchSerializationVersionForHeight(int height) {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
Triple<byte[], Integer, Integer> serializedBlock = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBlock == null) {
|
||||
return null;
|
||||
}
|
||||
Integer serializationVersion = serializedBlock.getB();
|
||||
return serializationVersion;
|
||||
}
|
||||
|
||||
public BlockTransformation fetchBlockAtHeight(int height) {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
byte[] serializedBytes = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBytes == null) {
|
||||
Triple<byte[], Integer, Integer> serializedBlock = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBlock == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] serializedBytes = serializedBlock.getA();
|
||||
Integer serializationVersion = serializedBlock.getB();
|
||||
if (serializedBytes == null || serializationVersion == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(serializedBytes);
|
||||
BlockTransformation blockInfo = null;
|
||||
try {
|
||||
blockInfo = BlockTransformer.fromByteBuffer(byteBuffer);
|
||||
switch (serializationVersion) {
|
||||
case 1:
|
||||
blockInfo = BlockTransformer.fromByteBuffer(byteBuffer);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
blockInfo = BlockTransformer.fromByteBufferV2(byteBuffer);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Invalid serialization version
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blockInfo != null && blockInfo.getBlockData() != null) {
|
||||
// Block height is stored outside of the main serialized bytes, so it
|
||||
// won't be set automatically.
|
||||
@@ -168,15 +196,20 @@ public class BlockArchiveReader {
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] fetchSerializedBlockBytesForSignature(byte[] signature, boolean includeHeightPrefix, Repository repository) {
|
||||
public Triple<byte[], Integer, Integer> fetchSerializedBlockBytesForSignature(byte[] signature, boolean includeHeightPrefix, Repository repository) {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
Integer height = this.fetchHeightForSignature(signature, repository);
|
||||
if (height != null) {
|
||||
byte[] blockBytes = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (blockBytes == null) {
|
||||
Triple<byte[], Integer, Integer> serializedBlock = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBlock == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] blockBytes = serializedBlock.getA();
|
||||
Integer version = serializedBlock.getB();
|
||||
if (blockBytes == null || version == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -187,18 +220,18 @@ public class BlockArchiveReader {
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(height));
|
||||
bytes.write(blockBytes);
|
||||
return bytes.toByteArray();
|
||||
return new Triple<>(bytes.toByteArray(), version, height);
|
||||
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return blockBytes;
|
||||
return new Triple<>(blockBytes, version, height);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] fetchSerializedBlockBytesForHeight(int height) {
|
||||
public Triple<byte[], Integer, Integer> fetchSerializedBlockBytesForHeight(int height) {
|
||||
String filename = this.getFilenameForHeight(height);
|
||||
if (filename == null) {
|
||||
// We don't have this block in the archive
|
||||
@@ -221,7 +254,7 @@ public class BlockArchiveReader {
|
||||
// End of fixed length header
|
||||
|
||||
// Make sure the version is one we recognize
|
||||
if (version != 1) {
|
||||
if (version != 1 && version != 2) {
|
||||
LOGGER.info("Error: unknown version in file {}: {}", filename, version);
|
||||
return null;
|
||||
}
|
||||
@@ -258,7 +291,7 @@ public class BlockArchiveReader {
|
||||
byte[] blockBytes = new byte[blockLength];
|
||||
file.read(blockBytes);
|
||||
|
||||
return blockBytes;
|
||||
return new Triple<>(blockBytes, version, height);
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
LOGGER.info("File {} not found: {}", filename, e.getMessage());
|
||||
@@ -279,6 +312,30 @@ public class BlockArchiveReader {
|
||||
}
|
||||
}
|
||||
|
||||
public int getHeightOfLastArchivedBlock() {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
int maxEndHeight = 0;
|
||||
|
||||
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 endHeight = heightInfo.getB();
|
||||
|
||||
if (endHeight != null && endHeight > maxEndHeight) {
|
||||
maxEndHeight = endHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return maxEndHeight;
|
||||
}
|
||||
|
||||
public void invalidateFileListCache() {
|
||||
this.fileListCache = null;
|
||||
}
|
||||
|
@@ -6,10 +6,13 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
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.BlockTransformation;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -18,6 +21,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
public class BlockArchiveWriter {
|
||||
|
||||
@@ -28,25 +32,78 @@ public class BlockArchiveWriter {
|
||||
BLOCK_NOT_FOUND
|
||||
}
|
||||
|
||||
public enum BlockArchiveDataSource {
|
||||
BLOCK_REPOSITORY, // To build an archive from the Blocks table
|
||||
BLOCK_ARCHIVE // To build a new archive from an existing archive
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiveWriter.class);
|
||||
|
||||
public static final long DEFAULT_FILE_SIZE_TARGET = 100 * 1024 * 1024; // 100MiB
|
||||
public static final long DEFAULT_FILE_SIZE_TARGET_V1 = 100 * 1024 * 1024; // 100MiB
|
||||
public static final long DEFAULT_FILE_SIZE_TARGET_V2 = 10 * 1024 * 1024; // 10MiB
|
||||
|
||||
private int startHeight;
|
||||
private final int endHeight;
|
||||
private final Integer serializationVersion;
|
||||
private final Path archivePath;
|
||||
private final Repository repository;
|
||||
|
||||
private long fileSizeTarget = DEFAULT_FILE_SIZE_TARGET;
|
||||
private long fileSizeTarget = DEFAULT_FILE_SIZE_TARGET_V1;
|
||||
private boolean shouldEnforceFileSizeTarget = true;
|
||||
|
||||
// Default data source to BLOCK_REPOSITORY; can optionally be overridden
|
||||
private BlockArchiveDataSource dataSource = BlockArchiveDataSource.BLOCK_REPOSITORY;
|
||||
|
||||
private boolean shouldLogProgress = false;
|
||||
|
||||
private int writtenCount;
|
||||
private int lastWrittenHeight;
|
||||
private Path outputPath;
|
||||
|
||||
public BlockArchiveWriter(int startHeight, int endHeight, Repository repository) {
|
||||
/**
|
||||
* Instantiate a BlockArchiveWriter using a custom archive path
|
||||
* @param startHeight
|
||||
* @param endHeight
|
||||
* @param repository
|
||||
*/
|
||||
public BlockArchiveWriter(int startHeight, int endHeight, Integer serializationVersion, Path archivePath, Repository repository) {
|
||||
this.startHeight = startHeight;
|
||||
this.endHeight = endHeight;
|
||||
this.archivePath = archivePath.toAbsolutePath();
|
||||
this.repository = repository;
|
||||
|
||||
if (serializationVersion == null) {
|
||||
// When serialization version isn't specified, fetch it from the existing archive
|
||||
serializationVersion = this.findSerializationVersion();
|
||||
}
|
||||
|
||||
// Reduce default file size target if we're using V2, as the average block size is over 90% smaller
|
||||
if (serializationVersion == 2) {
|
||||
this.setFileSizeTarget(DEFAULT_FILE_SIZE_TARGET_V2);
|
||||
}
|
||||
|
||||
this.serializationVersion = serializationVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a BlockArchiveWriter using the default archive path and version
|
||||
* @param startHeight
|
||||
* @param endHeight
|
||||
* @param repository
|
||||
*/
|
||||
public BlockArchiveWriter(int startHeight, int endHeight, Repository repository) {
|
||||
this(startHeight, endHeight, null, Paths.get(Settings.getInstance().getRepositoryPath(), "archive"), repository);
|
||||
}
|
||||
|
||||
private int findSerializationVersion() {
|
||||
// Attempt to fetch the serialization version from the existing archive
|
||||
Integer block2SerializationVersion = BlockArchiveReader.getInstance().fetchSerializationVersionForHeight(2);
|
||||
if (block2SerializationVersion != null) {
|
||||
return block2SerializationVersion;
|
||||
}
|
||||
|
||||
// Default to version specified in settings
|
||||
return Settings.getInstance().getDefaultArchiveVersion();
|
||||
}
|
||||
|
||||
public static int getMaxArchiveHeight(Repository repository) throws DataException {
|
||||
@@ -72,8 +129,7 @@ public class BlockArchiveWriter {
|
||||
|
||||
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();
|
||||
// This is generally a subfolder of the db directory, to make bootstrapping easier
|
||||
try {
|
||||
Files.createDirectories(archivePath);
|
||||
} catch (IOException e) {
|
||||
@@ -95,13 +151,13 @@ public class BlockArchiveWriter {
|
||||
|
||||
LOGGER.info(String.format("Fetching blocks from height %d...", startHeight));
|
||||
int i = 0;
|
||||
while (headerBytes.size() + bytes.size() < this.fileSizeTarget
|
||||
|| this.shouldEnforceFileSizeTarget == false) {
|
||||
while (headerBytes.size() + bytes.size() < this.fileSizeTarget) {
|
||||
|
||||
if (Controller.isStopping()) {
|
||||
return BlockArchiveWriteResult.STOPPING;
|
||||
}
|
||||
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||
Thread.sleep(1000L);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -112,7 +168,28 @@ public class BlockArchiveWriter {
|
||||
|
||||
//LOGGER.info("Fetching block {}...", currentHeight);
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(currentHeight);
|
||||
BlockData blockData = null;
|
||||
List<TransactionData> transactions = null;
|
||||
List<ATStateData> atStates = null;
|
||||
byte[] atStatesHash = null;
|
||||
|
||||
switch (this.dataSource) {
|
||||
case BLOCK_ARCHIVE:
|
||||
BlockTransformation archivedBlock = BlockArchiveReader.getInstance().fetchBlockAtHeight(currentHeight);
|
||||
if (archivedBlock != null) {
|
||||
blockData = archivedBlock.getBlockData();
|
||||
transactions = archivedBlock.getTransactions();
|
||||
atStates = archivedBlock.getAtStates();
|
||||
atStatesHash = archivedBlock.getAtStatesHash();
|
||||
}
|
||||
break;
|
||||
|
||||
case BLOCK_REPOSITORY:
|
||||
default:
|
||||
blockData = repository.getBlockRepository().fromHeight(currentHeight);
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockData == null) {
|
||||
return BlockArchiveWriteResult.BLOCK_NOT_FOUND;
|
||||
}
|
||||
@@ -122,18 +199,50 @@ public class BlockArchiveWriter {
|
||||
repository.getBlockArchiveRepository().save(blockArchiveData);
|
||||
repository.saveChanges();
|
||||
|
||||
// Build the block
|
||||
Block block;
|
||||
if (atStatesHash != null) {
|
||||
block = new Block(repository, blockData, transactions, atStatesHash);
|
||||
}
|
||||
else if (atStates != null) {
|
||||
block = new Block(repository, blockData, transactions, atStates);
|
||||
}
|
||||
else {
|
||||
block = new Block(repository, blockData);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Get serialized block bytes
|
||||
byte[] blockBytes;
|
||||
switch (serializationVersion) {
|
||||
case 1:
|
||||
blockBytes = BlockTransformer.toBytes(block);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
blockBytes = BlockTransformer.toBytesV2(block);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new DataException("Invalid serialization version");
|
||||
}
|
||||
|
||||
// Write block length
|
||||
bytes.write(Ints.toByteArray(blockBytes.length));
|
||||
// Write block bytes
|
||||
bytes.write(blockBytes);
|
||||
|
||||
// Log every 1000 blocks
|
||||
if (this.shouldLogProgress && i % 1000 == 0) {
|
||||
LOGGER.info("Archived up to block height {}. Size of current file: {} bytes", currentHeight, (headerBytes.size() + bytes.size()));
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
}
|
||||
@@ -147,11 +256,10 @@ public class BlockArchiveWriter {
|
||||
|
||||
// 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));
|
||||
fileOutputStream.write(Ints.toByteArray(serializationVersion));
|
||||
// Write start height
|
||||
fileOutputStream.write(Ints.toByteArray(startHeight));
|
||||
// Write end height
|
||||
@@ -199,4 +307,12 @@ public class BlockArchiveWriter {
|
||||
this.shouldEnforceFileSizeTarget = shouldEnforceFileSizeTarget;
|
||||
}
|
||||
|
||||
public void setDataSource(BlockArchiveDataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public void setShouldLogProgress(boolean shouldLogProgress) {
|
||||
this.shouldLogProgress = shouldLogProgress;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,6 +6,8 @@ import org.qortal.data.chat.ActiveChats;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
public interface ChatRepository {
|
||||
|
||||
/**
|
||||
@@ -15,10 +17,11 @@ public interface ChatRepository {
|
||||
*/
|
||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after,
|
||||
Integer txGroupId, byte[] reference, byte[] chatReferenceBytes, Boolean hasChatReference,
|
||||
List<String> involving, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
List<String> involving, String senderAddress, Encoding encoding,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException;
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData, Encoding encoding) throws DataException;
|
||||
|
||||
public ActiveChats getActiveChats(String address) throws DataException;
|
||||
public ActiveChats getActiveChats(String address, Encoding encoding) throws DataException;
|
||||
|
||||
}
|
||||
|
@@ -2,11 +2,6 @@ package org.qortal.repository;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.gui.SplashFrame;
|
||||
import org.qortal.repository.hsqldb.HSQLDBDatabaseArchiving;
|
||||
import org.qortal.repository.hsqldb.HSQLDBDatabasePruning;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepository;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -61,62 +56,6 @@ public abstract class RepositoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean archive(Repository repository) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes have no blockchain
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bulk archive the database the first time we use archive mode
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
if (RepositoryManager.canArchiveOrPrune()) {
|
||||
try {
|
||||
return HSQLDBDatabaseArchiving.buildBlockArchive(repository, BlockArchiveWriter.DEFAULT_FILE_SIZE_TARGET);
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to build block archive. 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.");
|
||||
LOGGER.info("To bootstrap, stop the core and delete the db folder, then start the core again.");
|
||||
SplashFrame.getInstance().updateStatus("Missing index. Bootstrapping is recommended.");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean prune(Repository repository) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes have no blockchain
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bulk prune the database the first time we use top-only or block archive mode
|
||||
if (Settings.getInstance().isTopOnly() ||
|
||||
Settings.getInstance().isArchiveEnabled()) {
|
||||
if (RepositoryManager.canArchiveOrPrune()) {
|
||||
try {
|
||||
boolean prunedATStates = HSQLDBDatabasePruning.pruneATStates((HSQLDBRepository) repository);
|
||||
boolean prunedBlocks = HSQLDBDatabasePruning.pruneBlocks((HSQLDBRepository) repository);
|
||||
|
||||
// Perform repository maintenance to shrink the db size down
|
||||
if (prunedATStates && prunedBlocks) {
|
||||
HSQLDBDatabasePruning.performMaintenance(repository);
|
||||
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) {
|
||||
quickCheckpointRequested = quick;
|
||||
}
|
||||
|
@@ -9,6 +9,8 @@ public interface VotingRepository {
|
||||
|
||||
// Polls
|
||||
|
||||
public List<PollData> getAllPolls(Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public PollData fromPollName(String pollName) throws DataException;
|
||||
|
||||
public boolean pollExists(String pollName) throws DataException;
|
||||
|
@@ -202,7 +202,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
|
||||
int version = resultSet.getInt(11);
|
||||
int nonce = resultSet.getInt(12);
|
||||
Service serviceResult = Service.valueOf(resultSet.getInt(13));
|
||||
int serviceInt = resultSet.getInt(13);
|
||||
int size = resultSet.getInt(14);
|
||||
boolean isDataRaw = resultSet.getBoolean(15); // NOT NULL, so no null to false
|
||||
DataType dataType = isDataRaw ? DataType.RAW_DATA : DataType.DATA_HASH;
|
||||
@@ -216,7 +216,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
// FUTURE: get payments from signature if needed. Avoiding for now to reduce database calls.
|
||||
|
||||
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
|
||||
version, serviceResult, nonce, size, nameResult, identifierResult, method, secret,
|
||||
version, serviceInt, nonce, size, nameResult, identifierResult, method, secret,
|
||||
compression, data, dataType, metadataHash, null);
|
||||
|
||||
arbitraryTransactionData.add(transactionData);
|
||||
@@ -277,7 +277,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
|
||||
int version = resultSet.getInt(11);
|
||||
int nonce = resultSet.getInt(12);
|
||||
Service serviceResult = Service.valueOf(resultSet.getInt(13));
|
||||
int serviceInt = resultSet.getInt(13);
|
||||
int size = resultSet.getInt(14);
|
||||
boolean isDataRaw = resultSet.getBoolean(15); // NOT NULL, so no null to false
|
||||
DataType dataType = isDataRaw ? DataType.RAW_DATA : DataType.DATA_HASH;
|
||||
@@ -291,7 +291,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
// FUTURE: get payments from signature if needed. Avoiding for now to reduce database calls.
|
||||
|
||||
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
|
||||
version, serviceResult, nonce, size, nameResult, identifierResult, methodResult, secret,
|
||||
version, serviceInt, nonce, size, nameResult, identifierResult, methodResult, secret,
|
||||
compression, data, dataType, metadataHash, null);
|
||||
|
||||
return transactionData;
|
||||
|
@@ -14,6 +14,8 @@ import org.qortal.repository.ChatRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
public class HSQLDBChatRepository implements ChatRepository {
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
@@ -24,8 +26,8 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
|
||||
@Override
|
||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes,
|
||||
byte[] chatReferenceBytes, Boolean hasChatReference, List<String> involving,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
byte[] chatReferenceBytes, Boolean hasChatReference, List<String> involving, String senderAddress,
|
||||
Encoding encoding, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
// Check args meet expectations
|
||||
if ((txGroupId != null && involving != null && !involving.isEmpty())
|
||||
|| (txGroupId == null && (involving == null || involving.size() != 2)))
|
||||
@@ -74,6 +76,11 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
whereClauses.add("chat_reference IS NULL");
|
||||
}
|
||||
|
||||
if (senderAddress != null) {
|
||||
whereClauses.add("sender = ?");
|
||||
bindParams.add(senderAddress);
|
||||
}
|
||||
|
||||
if (txGroupId != null) {
|
||||
whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally
|
||||
whereClauses.add("recipient IS NULL");
|
||||
@@ -122,7 +129,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
byte[] signature = resultSet.getBytes(13);
|
||||
|
||||
ChatMessage chatMessage = new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
||||
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
|
||||
senderName, recipient, recipientName, chatReference, encoding, data, isText, isEncrypted, signature);
|
||||
|
||||
chatMessages.add(chatMessage);
|
||||
} while (resultSet.next());
|
||||
@@ -134,7 +141,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException {
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData, Encoding encoding) throws DataException {
|
||||
String sql = "SELECT SenderNames.name, RecipientNames.name "
|
||||
+ "FROM ChatTransactions "
|
||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
||||
@@ -161,21 +168,22 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
byte[] signature = chatTransactionData.getSignature();
|
||||
|
||||
return new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
||||
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
|
||||
senderName, recipient, recipientName, chatReference, encoding, data,
|
||||
isText, isEncrypted, signature);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch convert chat transaction from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActiveChats getActiveChats(String address) throws DataException {
|
||||
List<GroupChat> groupChats = getActiveGroupChats(address);
|
||||
public ActiveChats getActiveChats(String address, Encoding encoding) throws DataException {
|
||||
List<GroupChat> groupChats = getActiveGroupChats(address, encoding);
|
||||
List<DirectChat> directChats = getActiveDirectChats(address);
|
||||
|
||||
return new ActiveChats(groupChats, directChats);
|
||||
}
|
||||
|
||||
private List<GroupChat> getActiveGroupChats(String address) throws DataException {
|
||||
private List<GroupChat> getActiveGroupChats(String address, Encoding encoding) throws DataException {
|
||||
// Find groups where address is a member and potential latest message details
|
||||
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name, signature, data "
|
||||
+ "FROM GroupMembers "
|
||||
@@ -208,7 +216,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
byte[] signature = resultSet.getBytes(6);
|
||||
byte[] data = resultSet.getBytes(7);
|
||||
|
||||
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName, signature, data);
|
||||
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName, signature, encoding, data);
|
||||
groupChats.add(groupChat);
|
||||
} while (resultSet.next());
|
||||
}
|
||||
@@ -242,7 +250,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
data = resultSet.getBytes(5);
|
||||
}
|
||||
|
||||
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName, signature, data);
|
||||
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName, signature, encoding, data);
|
||||
groupChats.add(groupChat);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch active group chats from repository", e);
|
||||
|
@@ -1,88 +0,0 @@
|
||||
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.gui.SplashFrame;
|
||||
import org.qortal.repository.BlockArchiveWriter;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
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(Repository repository, long fileSizeTarget) throws DataException {
|
||||
|
||||
// Only build the archive if we haven't already got one that is up to date
|
||||
boolean upToDate = BlockArchiveWriter.isArchiverUpToDate(repository);
|
||||
if (upToDate) {
|
||||
// Already archived
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.info("Building block archive - this process could take a while...");
|
||||
SplashFrame.getInstance().updateStatus("Building block archive...");
|
||||
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
int startHeight = 0;
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
try {
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(startHeight, maximumArchiveHeight, repository);
|
||||
writer.setFileSizeTarget(fileSizeTarget);
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
switch (result) {
|
||||
case OK:
|
||||
// Increment block archive height
|
||||
startHeight = writer.getLastWrittenHeight() + 1;
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,332 +0,0 @@
|
||||
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.data.block.BlockData;
|
||||
import org.qortal.gui.SplashFrame;
|
||||
import org.qortal.repository.BlockArchiveWriter;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
*
|
||||
* When switching from a full node to a pruning node, we need to delete most of the database contents.
|
||||
* 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 transferring only the necessary rows to a new table and then
|
||||
* deleting the original table, this makes the process much faster. It was taking several days to
|
||||
* delete the AT states in the background, but only a couple of minutes to copy them to a new table.
|
||||
*
|
||||
* The trade off is that we have to go through a form of "reshape" when starting the app for the first
|
||||
* time after enabling pruning mode. But given that this is an opt-in mode, I don't think it will be
|
||||
* a problem.
|
||||
*
|
||||
* Once the pruning is complete, it automatically performs a CHECKPOINT DEFRAG in order to
|
||||
* shrink the database file size down to a fraction of what it was before.
|
||||
*
|
||||
* From this point, the original background process will run, but can be dialled right down so not
|
||||
* to interfere with syncing.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
public class HSQLDBDatabasePruning {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBDatabasePruning.class);
|
||||
|
||||
|
||||
public static boolean pruneATStates(HSQLDBRepository repository) throws SQLException, DataException {
|
||||
|
||||
// Only bulk prune AT states if we have never done so before
|
||||
int pruneHeight = repository.getATRepository().getAtPruneHeight();
|
||||
if (pruneHeight > 0) {
|
||||
// Already pruned AT states
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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)");
|
||||
SplashFrame.getInstance().updateStatus("Pruning database (takes up to 30 mins)...");
|
||||
|
||||
// Create new AT-states table to hold smaller dataset
|
||||
repository.executeCheckedUpdate("DROP TABLE IF EXISTS ATStatesNew");
|
||||
repository.executeCheckedUpdate("CREATE TABLE ATStatesNew ("
|
||||
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
|
||||
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, sleep_until_message_timestamp BIGINT, "
|
||||
+ "PRIMARY KEY (AT_address, height), "
|
||||
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||
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();
|
||||
if (latestBlock == null) {
|
||||
LOGGER.info("Unable to determine blockchain height, necessary for bulk block pruning");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate some constants for later use
|
||||
final int blockchainHeight = latestBlock.getHeight();
|
||||
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 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(endHeight);
|
||||
|
||||
|
||||
// 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) {
|
||||
final int batchEndHeight = height + blockStep - 1;
|
||||
//LOGGER.info(String.format("Copying AT states between %d and %d...", height, batchEndHeight));
|
||||
|
||||
String sql = "SELECT height, AT_address FROM LatestATStates WHERE height BETWEEN ? AND ?";
|
||||
try (ResultSet latestAtStatesResultSet = repository.checkedExecute(sql, height, batchEndHeight)) {
|
||||
if (latestAtStatesResultSet != null) {
|
||||
do {
|
||||
int latestAtHeight = latestAtStatesResultSet.getInt(1);
|
||||
String latestAtAddress = latestAtStatesResultSet.getString(2);
|
||||
|
||||
// Copy this latest ATState to the new table
|
||||
//LOGGER.info(String.format("Copying AT %s at height %d...", latestAtAddress, latestAtHeight));
|
||||
try {
|
||||
String updateSql = "INSERT INTO ATStatesNew ("
|
||||
+ "SELECT AT_address, height, state_hash, fees, is_initial, sleep_until_message_timestamp "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE height = ? AND AT_address = ?)";
|
||||
repository.executeCheckedUpdate(updateSql, latestAtHeight, latestAtAddress);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to copy ATStates", e);
|
||||
}
|
||||
|
||||
// If this batch includes blocks after the maximum block to trim, we will need to copy
|
||||
// each of its AT states above maximumBlockToTrim as they are considered "recent". We
|
||||
// need to do this for _all_ AT states in these blocks, regardless of their latest state.
|
||||
if (batchEndHeight >= maximumBlockToTrim) {
|
||||
// Now copy this AT's states for each recent block they are present in
|
||||
for (int i = maximumBlockToTrim; i < endHeight; i++) {
|
||||
if (latestAtHeight < i) {
|
||||
// This AT finished before this block so there is nothing to copy
|
||||
continue;
|
||||
}
|
||||
|
||||
//LOGGER.info(String.format("Copying recent AT %s at height %d...", latestAtAddress, i));
|
||||
try {
|
||||
// Copy each LatestATState to the new table
|
||||
String updateSql = "INSERT IGNORE INTO ATStatesNew ("
|
||||
+ "SELECT AT_address, height, state_hash, fees, is_initial, sleep_until_message_timestamp "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE height = ? AND AT_address = ?)";
|
||||
repository.executeCheckedUpdate(updateSql, i, latestAtAddress);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to copy ATStates", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
repository.saveChanges();
|
||||
|
||||
} while (latestAtStatesResultSet.next());
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to copy AT states", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
int nextPruneHeight = maximumBlockToTrim + 1;
|
||||
repository.getATRepository().setAtPruneHeight(nextPruneHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
// Now prune/trim the ATStatesData, as this currently goes back over a month
|
||||
return HSQLDBDatabasePruning.pruneATStateData(repository);
|
||||
}
|
||||
|
||||
/*
|
||||
* Bulk prune ATStatesData to catch up with the now pruned ATStates table
|
||||
* This uses the existing AT States trimming code but with a much higher end block
|
||||
*/
|
||||
private static boolean pruneATStateData(Repository repository) throws DataException {
|
||||
|
||||
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();
|
||||
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 (or our archive height)
|
||||
|
||||
if (Controller.isStopping()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Override batch size in the settings because this is a one-off process
|
||||
final int batchSize = 1000;
|
||||
final int rowLimitPerBatch = 50000;
|
||||
int upperBatchHeight = pruneStartHeight + batchSize;
|
||||
int upperPruneHeight = Math.min(upperBatchHeight, upperPrunableHeight);
|
||||
|
||||
LOGGER.trace(String.format("Pruning AT states data between %d and %d...", pruneStartHeight, upperPruneHeight));
|
||||
|
||||
int numATStatesPruned = repository.getATRepository().trimAtStates(pruneStartHeight, upperPruneHeight, rowLimitPerBatch);
|
||||
repository.saveChanges();
|
||||
|
||||
if (numATStatesPruned > 0) {
|
||||
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;
|
||||
}
|
||||
else {
|
||||
// We've finished pruning
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean pruneBlocks(Repository repository) throws SQLException, DataException {
|
||||
|
||||
// Only bulk prune AT states if we have never done so before
|
||||
int pruneHeight = repository.getBlockRepository().getBlockPruneHeight();
|
||||
if (pruneHeight > 0) {
|
||||
// Already pruned blocks
|
||||
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();
|
||||
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) {
|
||||
// Prune all blocks up until our latest minus pruneBlockLimit
|
||||
|
||||
int upperBatchHeight = pruneStartHeight + Settings.getInstance().getBlockPruneBatchSize();
|
||||
int upperPruneHeight = Math.min(upperBatchHeight, upperPrunableHeight);
|
||||
|
||||
LOGGER.info(String.format("Pruning blocks between %d and %d...", pruneStartHeight, upperPruneHeight));
|
||||
|
||||
int numBlocksPruned = repository.getBlockRepository().pruneBlocks(pruneStartHeight, upperPruneHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
if (numBlocksPruned > 0) {
|
||||
LOGGER.info(String.format("Pruned %d block%s between %d and %d",
|
||||
numBlocksPruned, (numBlocksPruned != 1 ? "s" : ""),
|
||||
pruneStartHeight, upperPruneHeight));
|
||||
} else {
|
||||
final int nextPruneHeight = upperPruneHeight + 1;
|
||||
repository.getBlockRepository().setBlockPruneHeight(nextPruneHeight);
|
||||
repository.saveChanges();
|
||||
LOGGER.debug(String.format("Bumping block base prune height to %d", nextPruneHeight));
|
||||
|
||||
// Can we move onto next batch?
|
||||
if (upperPrunableHeight > nextPruneHeight) {
|
||||
pruneStartHeight = nextPruneHeight;
|
||||
}
|
||||
else {
|
||||
// We've finished pruning
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void performMaintenance(Repository repository) throws SQLException, DataException {
|
||||
try {
|
||||
SplashFrame.getInstance().updateStatus("Performing maintenance...");
|
||||
|
||||
// Timeout if the database isn't ready for backing up after 5 minutes
|
||||
// Nothing else should be using the db at this point, so a timeout shouldn't happen
|
||||
long timeout = 5 * 60 * 1000L;
|
||||
repository.performPeriodicMaintenance(timeout);
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
LOGGER.info("Attempt to perform maintenance failed due to timeout: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -21,6 +21,55 @@ public class HSQLDBVotingRepository implements VotingRepository {
|
||||
|
||||
// Polls
|
||||
|
||||
@Override
|
||||
public List<PollData> getAllPolls(Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
|
||||
sql.append("SELECT poll_name, description, creator, owner, published_when FROM Polls ORDER BY poll_name");
|
||||
|
||||
if (reverse != null && reverse)
|
||||
sql.append(" DESC");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<PollData> polls = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
|
||||
if (resultSet == null)
|
||||
return polls;
|
||||
|
||||
do {
|
||||
String pollName = resultSet.getString(1);
|
||||
String description = resultSet.getString(2);
|
||||
byte[] creatorPublicKey = resultSet.getBytes(3);
|
||||
String owner = resultSet.getString(4);
|
||||
long published = resultSet.getLong(5);
|
||||
|
||||
String optionsSql = "SELECT option_name FROM PollOptions WHERE poll_name = ? ORDER BY option_index ASC";
|
||||
try (ResultSet optionsResultSet = this.repository.checkedExecute(optionsSql, pollName)) {
|
||||
if (optionsResultSet == null)
|
||||
return null;
|
||||
|
||||
List<PollOptionData> pollOptions = new ArrayList<>();
|
||||
|
||||
// NOTE: do-while because checkedExecute() above has already called rs.next() for us
|
||||
do {
|
||||
String optionName = optionsResultSet.getString(1);
|
||||
|
||||
pollOptions.add(new PollOptionData(optionName));
|
||||
} while (optionsResultSet.next());
|
||||
|
||||
polls.add(new PollData(creatorPublicKey, owner, pollName, description, pollOptions, published));
|
||||
}
|
||||
|
||||
} while (resultSet.next());
|
||||
|
||||
return polls;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch polls from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PollData fromPollName(String pollName) throws DataException {
|
||||
String sql = "SELECT description, creator, owner, published_when FROM Polls WHERE poll_name = ?";
|
||||
|
@@ -31,7 +31,7 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
|
||||
|
||||
int version = resultSet.getInt(1);
|
||||
int nonce = resultSet.getInt(2);
|
||||
Service service = Service.valueOf(resultSet.getInt(3));
|
||||
int serviceInt = resultSet.getInt(3);
|
||||
int size = resultSet.getInt(4);
|
||||
boolean isDataRaw = resultSet.getBoolean(5); // NOT NULL, so no null to false
|
||||
DataType dataType = isDataRaw ? DataType.RAW_DATA : DataType.DATA_HASH;
|
||||
@@ -44,7 +44,7 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
|
||||
ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.valueOf(resultSet.getInt(12));
|
||||
|
||||
List<PaymentData> payments = this.getPaymentsFromSignature(baseTransactionData.getSignature());
|
||||
return new ArbitraryTransactionData(baseTransactionData, version, service, nonce, size, name,
|
||||
return new ArbitraryTransactionData(baseTransactionData, version, serviceInt, nonce, size, name,
|
||||
identifier, method, secret, compression, data, dataType, metadataHash, payments);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch arbitrary transaction from repository", e);
|
||||
@@ -66,7 +66,7 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("ArbitraryTransactions");
|
||||
|
||||
saveHelper.bind("signature", arbitraryTransactionData.getSignature()).bind("sender", arbitraryTransactionData.getSenderPublicKey())
|
||||
.bind("version", arbitraryTransactionData.getVersion()).bind("service", arbitraryTransactionData.getService().value)
|
||||
.bind("version", arbitraryTransactionData.getVersion()).bind("service", arbitraryTransactionData.getServiceInt())
|
||||
.bind("nonce", arbitraryTransactionData.getNonce()).bind("size", arbitraryTransactionData.getSize())
|
||||
.bind("is_data_raw", arbitraryTransactionData.getDataType() == DataType.RAW_DATA).bind("data", arbitraryTransactionData.getData())
|
||||
.bind("metadata_hash", arbitraryTransactionData.getMetadataHash()).bind("name", arbitraryTransactionData.getName())
|
||||
|
@@ -178,6 +178,8 @@ public class Settings {
|
||||
private boolean archiveEnabled = true;
|
||||
/** How often to attempt archiving (ms). */
|
||||
private long archiveInterval = 7171L; // milliseconds
|
||||
/** Serialization version to use when building an archive */
|
||||
private int defaultArchiveVersion = 1;
|
||||
|
||||
|
||||
/** Whether to automatically bootstrap instead of syncing from genesis */
|
||||
@@ -215,7 +217,7 @@ public class Settings {
|
||||
public long recoveryModeTimeout = 10 * 60 * 1000L;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "3.8.2";
|
||||
private String minPeerVersion = "3.8.7";
|
||||
/** Whether to allow connections with peers below minPeerVersion
|
||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||
@@ -273,6 +275,7 @@ public class Settings {
|
||||
private String[] bootstrapHosts = new String[] {
|
||||
"http://bootstrap.qortal.org",
|
||||
"http://bootstrap2.qortal.org",
|
||||
"http://bootstrap3.qortal.org",
|
||||
"http://bootstrap.qortal.online"
|
||||
};
|
||||
|
||||
@@ -926,6 +929,10 @@ public class Settings {
|
||||
return this.archiveInterval;
|
||||
}
|
||||
|
||||
public int getDefaultArchiveVersion() {
|
||||
return this.defaultArchiveVersion;
|
||||
}
|
||||
|
||||
|
||||
public boolean getBootstrap() {
|
||||
return this.bootstrap;
|
||||
|
@@ -9,6 +9,7 @@ import org.qortal.account.Account;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.PaymentData;
|
||||
@@ -87,6 +88,12 @@ public class ArbitraryTransaction extends Transaction {
|
||||
if (this.transactionData.getFee() < 0)
|
||||
return ValidationResult.NEGATIVE_FEE;
|
||||
|
||||
// After the feature trigger, we require the fee to be sufficient if it's not 0.
|
||||
// If the fee is zero, then the nonce is validated in isSignatureValid() as an alternative to a fee
|
||||
if (this.arbitraryTransactionData.getTimestamp() >= BlockChain.getInstance().getArbitraryOptionalFeeTimestamp() && this.arbitraryTransactionData.getFee() != 0L) {
|
||||
return super.isFeeValid();
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@@ -207,10 +214,14 @@ public class ArbitraryTransaction extends Transaction {
|
||||
// Clear nonce from transactionBytes
|
||||
ArbitraryTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// We only need to check nonce for recent transactions due to PoW verification overhead
|
||||
if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) {
|
||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
// As of feature-trigger timestamp, we only require a nonce when the fee is zero
|
||||
boolean beforeFeatureTrigger = this.arbitraryTransactionData.getTimestamp() < BlockChain.getInstance().getArbitraryOptionalFeeTimestamp();
|
||||
if (beforeFeatureTrigger || this.arbitraryTransactionData.getFee() == 0L) {
|
||||
// We only need to check nonce for recent transactions due to PoW verification overhead
|
||||
if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) {
|
||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +252,15 @@ public class ArbitraryTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
if (arbitraryTransactionData.getName() == null)
|
||||
return;
|
||||
|
||||
// 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(arbitraryTransactionData.getName(), this.repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -312,16 +312,24 @@ public class BlockTransformer extends Transformer {
|
||||
ByteArrayOutputStream atHashBytes = new ByteArrayOutputStream(atBytesLength);
|
||||
long atFees = 0;
|
||||
|
||||
for (ATStateData atStateData : block.getATStates()) {
|
||||
// Skip initial states generated by DEPLOY_AT transactions in the same block
|
||||
if (atStateData.isInitial())
|
||||
continue;
|
||||
if (block.getAtStatesHash() != null) {
|
||||
// We already have the AT states hash
|
||||
atFees = blockData.getATFees();
|
||||
atHashBytes.write(block.getAtStatesHash());
|
||||
}
|
||||
else {
|
||||
// We need to build the AT states hash
|
||||
for (ATStateData atStateData : block.getATStates()) {
|
||||
// Skip initial states generated by DEPLOY_AT transactions in the same block
|
||||
if (atStateData.isInitial())
|
||||
continue;
|
||||
|
||||
atHashBytes.write(atStateData.getATAddress().getBytes(StandardCharsets.UTF_8));
|
||||
atHashBytes.write(atStateData.getStateHash());
|
||||
atHashBytes.write(Longs.toByteArray(atStateData.getFees()));
|
||||
atHashBytes.write(atStateData.getATAddress().getBytes(StandardCharsets.UTF_8));
|
||||
atHashBytes.write(atStateData.getStateHash());
|
||||
atHashBytes.write(Longs.toByteArray(atStateData.getFees()));
|
||||
|
||||
atFees += atStateData.getFees();
|
||||
atFees += atStateData.getFees();
|
||||
}
|
||||
}
|
||||
|
||||
bytes.write(Ints.toByteArray(blockData.getATCount()));
|
||||
|
@@ -7,7 +7,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Utf8;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
@@ -131,7 +130,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
|
||||
payments.add(PaymentTransformer.fromByteBuffer(byteBuffer));
|
||||
}
|
||||
|
||||
Service service = Service.valueOf(byteBuffer.getInt());
|
||||
int service = byteBuffer.getInt();
|
||||
|
||||
// We might be receiving hash of data instead of actual raw data
|
||||
boolean isRaw = byteBuffer.get() != 0;
|
||||
@@ -226,7 +225,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
|
||||
for (PaymentData paymentData : payments)
|
||||
bytes.write(PaymentTransformer.toBytes(paymentData));
|
||||
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getService().value));
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getServiceInt()));
|
||||
|
||||
bytes.write((byte) (arbitraryTransactionData.getDataType() == DataType.RAW_DATA ? 1 : 0));
|
||||
|
||||
@@ -299,7 +298,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
|
||||
bytes.write(PaymentTransformer.toBytes(paymentData));
|
||||
}
|
||||
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getService().value));
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getServiceInt()));
|
||||
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getData().length));
|
||||
|
||||
|
@@ -21,6 +21,16 @@ public class BlockArchiveUtils {
|
||||
* into the HSQLDB, in order to make it SQL-compatible
|
||||
* again.
|
||||
* <p>
|
||||
* This is only fully compatible with archives that use
|
||||
* serialization version 1. For version 2 (or above),
|
||||
* we are unable to import individual AT states as we
|
||||
* only have a single combined hash, so the use cases
|
||||
* for this are greatly limited.
|
||||
* <p>
|
||||
* A version 1 archive should ultimately be rebuildable
|
||||
* via a resync or reindex from genesis, allowing
|
||||
* access to this feature once again.
|
||||
* <p>
|
||||
* Note: calls discardChanges() and saveChanges(), so
|
||||
* make sure that you commit any existing repository
|
||||
* changes before calling this method.
|
||||
@@ -61,9 +71,18 @@ public class BlockArchiveUtils {
|
||||
repository.getBlockRepository().save(blockInfo.getBlockData());
|
||||
|
||||
// Save AT state data hashes
|
||||
for (ATStateData atStateData : blockInfo.getAtStates()) {
|
||||
atStateData.setHeight(blockInfo.getBlockData().getHeight());
|
||||
repository.getATRepository().save(atStateData);
|
||||
if (blockInfo.getAtStates() != null) {
|
||||
for (ATStateData atStateData : blockInfo.getAtStates()) {
|
||||
atStateData.setHeight(blockInfo.getBlockData().getHeight());
|
||||
repository.getATRepository().save(atStateData);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We don't have AT state hashes, so we are only importing a partial state.
|
||||
// This can still be useful to allow orphaning to very old blocks, when we
|
||||
// need to access other chainstate info (such as balances) at an earlier block.
|
||||
// In order to do this, the orphan process must be temporarily adjusted to avoid
|
||||
// orphaning AT states, as it will otherwise fail due to having no previous state.
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
|
@@ -85,7 +85,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 1092000,
|
||||
"selfSponsorshipAlgoV1Height": 1092400,
|
||||
"feeValidationFixTimestamp": 1671918000000,
|
||||
"chatReferenceTimestamp": 1674316800000
|
||||
"chatReferenceTimestamp": 1674316800000,
|
||||
"arbitraryOptionalFeeTimestamp": 1680278400000
|
||||
},
|
||||
"checkpoints": [
|
||||
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -10,8 +11,6 @@ import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.repository.hsqldb.HSQLDBDatabaseArchiving;
|
||||
import org.qortal.repository.hsqldb.HSQLDBDatabasePruning;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.test.common.AtUtils;
|
||||
@@ -26,7 +25,6 @@ import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.SQLException;
|
||||
@@ -34,13 +32,16 @@ import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BlockArchiveTests extends Common {
|
||||
public class BlockArchiveV1Tests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
public void beforeTest() throws DataException, IllegalAccessException {
|
||||
Common.useSettings("test-settings-v2-block-archive.json");
|
||||
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
|
||||
this.deleteArchiveDirectory();
|
||||
|
||||
// Set default archive version to 1, so that archive builds in these tests use V2
|
||||
FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 1, true);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -333,212 +334,6 @@ public class BlockArchiveTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBulkArchiveAndPrune() throws DataException, SQLException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
HSQLDBRepository hsqldb = (HSQLDBRepository) repository;
|
||||
|
||||
// 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, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Check the current archive height
|
||||
assertEquals(0, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Write blocks 2-900 to the archive (using bulk method)
|
||||
int fileSizeTarget = 428600; // Pre-calculated size of 900 blocks
|
||||
assertTrue(HSQLDBDatabaseArchiving.buildBlockArchive(repository, fileSizeTarget));
|
||||
|
||||
// Ensure the block archive height has increased
|
||||
assertEquals(901, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Ensure the SQL repository contains blocks 2 and 900...
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(2));
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(900));
|
||||
|
||||
// Check the current prune heights
|
||||
assertEquals(0, repository.getBlockRepository().getBlockPruneHeight());
|
||||
assertEquals(0, repository.getATRepository().getAtPruneHeight());
|
||||
|
||||
// Prior to archiving or pruning, ensure blocks 2 to 1002 and their AT states are available in the db
|
||||
for (int i=2; i<=1002; i++) {
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(i));
|
||||
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(i);
|
||||
assertNotNull(atStates);
|
||||
assertEquals(1, atStates.size());
|
||||
}
|
||||
|
||||
// Prune all the archived blocks and AT states (using bulk method)
|
||||
assertTrue(HSQLDBDatabasePruning.pruneBlocks(hsqldb));
|
||||
assertTrue(HSQLDBDatabasePruning.pruneATStates(hsqldb));
|
||||
|
||||
// Ensure the current prune heights have increased
|
||||
assertEquals(901, repository.getBlockRepository().getBlockPruneHeight());
|
||||
assertEquals(901, repository.getATRepository().getAtPruneHeight());
|
||||
|
||||
// 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());
|
||||
|
||||
// Ensure blocks 2-900 are all available in the archive
|
||||
for (int i=2; i<=900; i++) {
|
||||
assertNotNull(repository.getBlockArchiveRepository().fromHeight(i));
|
||||
}
|
||||
|
||||
// Ensure blocks 2-900 are NOT available in the db
|
||||
for (int i=2; i<=900; i++) {
|
||||
assertNull(repository.getBlockRepository().fromHeight(i));
|
||||
}
|
||||
|
||||
// Ensure blocks 901 to 1002 and their AT states are available in the db
|
||||
for (int i=901; i<=1002; i++) {
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(i));
|
||||
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(i);
|
||||
assertNotNull(atStates);
|
||||
assertEquals(1, atStates.size());
|
||||
}
|
||||
|
||||
// Ensure blocks 901 to 1002 are not available in the archive
|
||||
for (int i=901; i<=1002; i++) {
|
||||
assertNull(repository.getBlockArchiveRepository().fromHeight(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBulkArchiveAndPruneMultipleFiles() throws DataException, SQLException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
HSQLDBRepository hsqldb = (HSQLDBRepository) repository;
|
||||
|
||||
// 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, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Check the current archive height
|
||||
assertEquals(0, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Write blocks 2-900 to the archive (using bulk method)
|
||||
int fileSizeTarget = 42360; // Pre-calculated size of approx 90 blocks
|
||||
assertTrue(HSQLDBDatabaseArchiving.buildBlockArchive(repository, fileSizeTarget));
|
||||
|
||||
// Ensure 10 archive files have been created
|
||||
Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive");
|
||||
assertEquals(10, new File(archivePath.toString()).list().length);
|
||||
|
||||
// Check the files exist
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "2-90.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "91-179.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "180-268.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "269-357.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "358-446.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "447-535.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "536-624.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "625-713.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "714-802.dat")));
|
||||
assertTrue(Files.exists(Paths.get(archivePath.toString(), "803-891.dat")));
|
||||
|
||||
// Ensure the block archive height has increased
|
||||
// It won't be as high as 901, because blocks 892-901 were too small to reach the file size
|
||||
// target of the 11th file
|
||||
assertEquals(892, repository.getBlockArchiveRepository().getBlockArchiveHeight());
|
||||
|
||||
// Ensure the SQL repository contains blocks 2 and 891...
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(2));
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(891));
|
||||
|
||||
// Check the current prune heights
|
||||
assertEquals(0, repository.getBlockRepository().getBlockPruneHeight());
|
||||
assertEquals(0, repository.getATRepository().getAtPruneHeight());
|
||||
|
||||
// Prior to archiving or pruning, ensure blocks 2 to 1002 and their AT states are available in the db
|
||||
for (int i=2; i<=1002; i++) {
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(i));
|
||||
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(i);
|
||||
assertNotNull(atStates);
|
||||
assertEquals(1, atStates.size());
|
||||
}
|
||||
|
||||
// Prune all the archived blocks and AT states (using bulk method)
|
||||
assertTrue(HSQLDBDatabasePruning.pruneBlocks(hsqldb));
|
||||
assertTrue(HSQLDBDatabasePruning.pruneATStates(hsqldb));
|
||||
|
||||
// Ensure the current prune heights have increased
|
||||
assertEquals(892, repository.getBlockRepository().getBlockPruneHeight());
|
||||
assertEquals(892, repository.getATRepository().getAtPruneHeight());
|
||||
|
||||
// Now ensure the SQL repository is missing blocks 2 and 891...
|
||||
assertNull(repository.getBlockRepository().fromHeight(2));
|
||||
assertNull(repository.getBlockRepository().fromHeight(891));
|
||||
|
||||
// ... 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(892));
|
||||
|
||||
// Validate the latest block height in the repository
|
||||
assertEquals(1002, (int) repository.getBlockRepository().getLastBlock().getHeight());
|
||||
|
||||
// Ensure blocks 2-891 are all available in the archive
|
||||
for (int i=2; i<=891; i++) {
|
||||
assertNotNull(repository.getBlockArchiveRepository().fromHeight(i));
|
||||
}
|
||||
|
||||
// Ensure blocks 2-891 are NOT available in the db
|
||||
for (int i=2; i<=891; i++) {
|
||||
assertNull(repository.getBlockRepository().fromHeight(i));
|
||||
}
|
||||
|
||||
// Ensure blocks 892 to 1002 and their AT states are available in the db
|
||||
for (int i=892; i<=1002; i++) {
|
||||
assertNotNull(repository.getBlockRepository().fromHeight(i));
|
||||
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(i);
|
||||
assertNotNull(atStates);
|
||||
assertEquals(1, atStates.size());
|
||||
}
|
||||
|
||||
// Ensure blocks 892 to 1002 are not available in the archive
|
||||
for (int i=892; i<=1002; i++) {
|
||||
assertNull(repository.getBlockArchiveRepository().fromHeight(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrimArchivePruneAndOrphan() throws DataException, InterruptedException, TransformationException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
504
src/test/java/org/qortal/test/BlockArchiveV2Tests.java
Normal file
504
src/test/java/org/qortal/test/BlockArchiveV2Tests.java
Normal file
@@ -0,0 +1,504 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.junit.After;
|
||||
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.data.transaction.TransactionData;
|
||||
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.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
import org.qortal.utils.BlockArchiveUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BlockArchiveV2Tests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException, IllegalAccessException {
|
||||
Common.useSettings("test-settings-v2-block-archive.json");
|
||||
NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset());
|
||||
this.deleteArchiveDirectory();
|
||||
|
||||
// Set default archive version to 2, so that archive builds in these tests use V2
|
||||
FieldUtils.writeField(Settings.getInstance(), "defaultArchiveVersion", 2, true);
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() throws DataException {
|
||||
this.deleteArchiveDirectory();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testWriter() throws DataException, InterruptedException, TransformationException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Mint some blocks so that we are able to archive them later
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
|
||||
// Mint some blocks so that we are able to archive them later
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
}
|
||||
|
||||
// 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();
|
||||
BlockTransformation block2Info = reader.fetchBlockAtHeight(2);
|
||||
BlockData block2ArchiveData = block2Info.getBlockData();
|
||||
|
||||
// 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
|
||||
BlockTransformation block900Info = reader.fetchBlockAtHeight(900);
|
||||
BlockData block900ArchiveData = block900Info.getBlockData();
|
||||
|
||||
// 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()) {
|
||||
|
||||
// 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 < 1000; i++) {
|
||||
BlockMinter.mintTestingBlock(repository, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
}
|
||||
|
||||
// 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();
|
||||
BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight);
|
||||
BlockData archivedBlockData = blockInfo.getBlockData();
|
||||
byte[] archivedAtStateHash = blockInfo.getAtStatesHash();
|
||||
List<TransactionData> archivedTransactions = blockInfo.getTransactions();
|
||||
|
||||
// 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) {
|
||||
assertEquals(1, archivedTransactions.size());
|
||||
assertEquals(Transaction.TransactionType.DEPLOY_AT, archivedTransactions.get(0).getType());
|
||||
}
|
||||
else {
|
||||
// Blocks 3+ shouldn't have any transactions
|
||||
assertTrue(archivedTransactions.isEmpty());
|
||||
}
|
||||
|
||||
// Ensure the archive has the AT states hash
|
||||
assertNotNull(archivedAtStateHash);
|
||||
|
||||
// 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());
|
||||
|
||||
// TODO: build atStatesHash and compare against value in archive
|
||||
}
|
||||
|
||||
// Check block 10 (unarchived)
|
||||
BlockArchiveReader reader = BlockArchiveReader.getInstance();
|
||||
BlockTransformation blockInfo = reader.fetchBlockAtHeight(10);
|
||||
assertNull(blockInfo);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArchiveAndPrune() throws DataException, InterruptedException, TransformationException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// 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, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
}
|
||||
|
||||
// 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(901);
|
||||
repository.saveChanges();
|
||||
assertEquals(901, 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(900);
|
||||
repository.saveChanges();
|
||||
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900);
|
||||
assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state
|
||||
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()) {
|
||||
|
||||
// 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, Common.getTestAccount(repository, "alice-reward-share"));
|
||||
}
|
||||
|
||||
// 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().rebuildLatestAtStates(500);
|
||||
repository.getATRepository().trimAtStates(0, 500, 1000);
|
||||
repository.getATRepository().setAtTrimHeight(501);
|
||||
|
||||
// Now block 499 should only have the AT state data hash
|
||||
List<ATStateData> block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499);
|
||||
atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499);
|
||||
assertNotNull(atStatesData.getStateHash());
|
||||
assertNull(atStatesData.getStateData());
|
||||
|
||||
// ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range
|
||||
block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500);
|
||||
atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500);
|
||||
assertNotNull(atStatesData.getStateHash());
|
||||
assertNotNull(atStatesData.getStateData());
|
||||
|
||||
// ... and block 501 should also 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(500);
|
||||
repository.saveChanges();
|
||||
int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500);
|
||||
assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state
|
||||
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 2 more block, which should be the last one that is possible to be orphaned
|
||||
// TODO: figure out why this is 1 block more than in the equivalent block archive V1 test
|
||||
BlockUtils.orphanBlocks(repository, 2);
|
||||
|
||||
// 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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -9,6 +9,7 @@ import org.qortal.crosschain.BitcoinACCTv1;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -417,7 +418,7 @@ public class RepositoryTests extends Common {
|
||||
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
String address = Crypto.toAddress(new byte[32]);
|
||||
|
||||
hsqldb.getChatRepository().getActiveChats(address);
|
||||
hsqldb.getChatRepository().getActiveChats(address, ChatMessage.Encoding.BASE58);
|
||||
} catch (DataException e) {
|
||||
fail("HSQLDB bug #1580");
|
||||
}
|
||||
|
@@ -246,7 +246,7 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
||||
Path path = Paths.get("src/test/resources/arbitrary/demo1");
|
||||
|
||||
ArbitraryDataTransactionBuilder txnBuilder = new ArbitraryDataTransactionBuilder(
|
||||
repository, publicKey58, path, name, Method.PUT, Service.ARBITRARY_DATA, null,
|
||||
repository, publicKey58, 0L, path, name, Method.PUT, Service.ARBITRARY_DATA, null,
|
||||
null, null, null, null);
|
||||
|
||||
txnBuilder.build();
|
||||
|
@@ -107,7 +107,7 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the chunk count is correct
|
||||
@@ -157,7 +157,7 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the chunk count is correct
|
||||
@@ -219,7 +219,7 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the chunk count is correct
|
||||
@@ -273,7 +273,7 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
// Create PUT transaction
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize,
|
||||
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
|
||||
title, description, tags, category);
|
||||
|
||||
// Check the metadata is correct
|
||||
|
@@ -5,12 +5,15 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.RegisterNameTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -20,11 +23,15 @@ import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.ArbitraryTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@@ -36,7 +43,7 @@ public class ArbitraryTransactionTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDifficultyTooLow() throws IllegalAccessException, DataException, IOException, MissingDataException {
|
||||
public void testDifficultyTooLow() throws IllegalAccessException, DataException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
@@ -78,7 +85,386 @@ public class ArbitraryTransactionTests extends Common {
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonceAndFee() throws IllegalAccessException, DataException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction, with a fee
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = 10000000; // sufficient
|
||||
boolean computeNonce = true;
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null);
|
||||
|
||||
// Check that nonce validation succeeds
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
|
||||
// Increase difficulty to 15
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true);
|
||||
|
||||
// Make sure that nonce validation still succeeds, as the fee has allowed us to avoid including a nonce
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonceAndLowFee() throws IllegalAccessException, DataException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction, with a fee that is too low
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = 9999999; // insufficient
|
||||
boolean computeNonce = true;
|
||||
boolean insufficientFeeDetected = false;
|
||||
try {
|
||||
ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null);
|
||||
}
|
||||
catch (DataException e) {
|
||||
if (e.getMessage().contains("INSUFFICIENT_FEE")) {
|
||||
insufficientFeeDetected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction should be invalid due to an insufficient fee
|
||||
assertTrue(insufficientFeeDetected);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeeNoNonce() throws IllegalAccessException, DataException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction, with a fee
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = 10000000; // sufficient
|
||||
boolean computeNonce = false;
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null);
|
||||
|
||||
// Check that nonce validation succeeds, even though it wasn't computed. This is because we have included a sufficient fee.
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
|
||||
// Increase difficulty to 15
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true);
|
||||
|
||||
// Make sure that nonce validation still succeeds, as the fee has allowed us to avoid including a nonce
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLowFeeNoNonce() throws IllegalAccessException, DataException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction, with a fee that is too low. Also, don't compute a nonce.
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = 9999999; // insufficient
|
||||
|
||||
ArbitraryDataTransactionBuilder txnBuilder = new ArbitraryDataTransactionBuilder(
|
||||
repository, publicKey58, fee, path1, name, ArbitraryTransactionData.Method.PUT, service, identifier, null, null, null, null);
|
||||
|
||||
txnBuilder.setChunkSize(chunkSize);
|
||||
txnBuilder.build();
|
||||
ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData();
|
||||
Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, alice);
|
||||
|
||||
// Transaction should be invalid due to an insufficient fee
|
||||
assertEquals(Transaction.ValidationResult.INSUFFICIENT_FEE, result);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testZeroFeeNoNonce() throws IllegalAccessException, DataException, IOException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction, with a fee that is too low. Also, don't compute a nonce.
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = 0L;
|
||||
|
||||
ArbitraryDataTransactionBuilder txnBuilder = new ArbitraryDataTransactionBuilder(
|
||||
repository, publicKey58, fee, path1, name, ArbitraryTransactionData.Method.PUT, service, identifier, null, null, null, null);
|
||||
|
||||
txnBuilder.setChunkSize(chunkSize);
|
||||
txnBuilder.build();
|
||||
ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData();
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
|
||||
// Transaction should be invalid
|
||||
assertFalse(arbitraryTransaction.isSignatureValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonceAndFeeBeforeFeatureTrigger() throws IllegalAccessException, DataException, IOException {
|
||||
// Use v2-minting settings, as these are pre-feature-trigger
|
||||
Common.useSettings("test-settings-v2-minting.json");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction, with a fee
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = 10000000; // sufficient
|
||||
boolean computeNonce = true;
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null);
|
||||
|
||||
// Check that nonce validation succeeds
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
|
||||
// Increase difficulty to 15
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true);
|
||||
|
||||
// Make sure the nonce validation fails, as we aren't allowing a fee to replace a nonce yet.
|
||||
// Note: there is a very tiny chance this could succeed due to being extremely lucky
|
||||
// and finding a high difficulty nonce in the first couple of cycles. It will be rare
|
||||
// enough that we shouldn't need to account for it.
|
||||
assertFalse(transaction.isSignatureValid());
|
||||
|
||||
// Reduce difficulty back to 1, to double check
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonceAndInsufficientFeeBeforeFeatureTrigger() throws IllegalAccessException, DataException, IOException {
|
||||
// Use v2-minting settings, as these are pre-feature-trigger
|
||||
Common.useSettings("test-settings-v2-minting.json");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction, with a fee
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = 9999999; // insufficient
|
||||
boolean computeNonce = true;
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null);
|
||||
|
||||
// Check that nonce validation succeeds
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
|
||||
// The transaction should be valid because we don't care about the fee (before the feature trigger)
|
||||
assertEquals(Transaction.ValidationResult.OK, transaction.isValidUnconfirmed());
|
||||
|
||||
// Increase difficulty to 15
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true);
|
||||
|
||||
// Make sure the nonce validation fails, as we aren't allowing a fee to replace a nonce yet (and it was insufficient anyway)
|
||||
// Note: there is a very tiny chance this could succeed due to being extremely lucky
|
||||
// and finding a high difficulty nonce in the first couple of cycles. It will be rare
|
||||
// enough that we shouldn't need to account for it.
|
||||
assertFalse(transaction.isSignatureValid());
|
||||
|
||||
// Reduce difficulty back to 1, to double check
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonceAndZeroFeeBeforeFeatureTrigger() throws IllegalAccessException, DataException, IOException {
|
||||
// Use v2-minting settings, as these are pre-feature-trigger
|
||||
Common.useSettings("test-settings-v2-minting.json");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||
String name = "TEST"; // Can be anything for this test
|
||||
String identifier = null; // Not used for this test
|
||||
Service service = Service.ARBITRARY_DATA;
|
||||
int chunkSize = 100;
|
||||
int dataLength = 900; // Actual data length will be longer due to encryption
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
|
||||
// Create PUT transaction, with a fee
|
||||
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
long fee = 0L;
|
||||
boolean computeNonce = true;
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, computeNonce, null, null, null, null);
|
||||
|
||||
// Check that nonce validation succeeds
|
||||
byte[] signature = arbitraryDataFile.getSignature();
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
|
||||
// The transaction should be valid because we don't care about the fee (before the feature trigger)
|
||||
assertEquals(Transaction.ValidationResult.OK, transaction.isValidUnconfirmed());
|
||||
|
||||
// Increase difficulty to 15
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true);
|
||||
|
||||
// Make sure the nonce validation fails, as we aren't allowing a fee to replace a nonce yet (and it was insufficient anyway)
|
||||
// Note: there is a very tiny chance this could succeed due to being extremely lucky
|
||||
// and finding a high difficulty nonce in the first couple of cycles. It will be rare
|
||||
// enough that we shouldn't need to account for it.
|
||||
assertFalse(transaction.isSignatureValid());
|
||||
|
||||
// Reduce difficulty back to 1, to double check
|
||||
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidService() {
|
||||
byte[] randomHash = new byte[32];
|
||||
new Random().nextBytes(randomHash);
|
||||
|
||||
byte[] lastReference = new byte[64];
|
||||
new Random().nextBytes(lastReference);
|
||||
|
||||
Long now = NTP.getTime();
|
||||
|
||||
final BaseTransactionData baseTransactionData = new BaseTransactionData(now, Group.NO_GROUP,
|
||||
lastReference, randomHash, 0L, null);
|
||||
final String name = "test";
|
||||
final String identifier = "test";
|
||||
final ArbitraryTransactionData.Method method = ArbitraryTransactionData.Method.PUT;
|
||||
final ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.ZIP;
|
||||
final int size = 999;
|
||||
final int version = 5;
|
||||
final int nonce = 0;
|
||||
final byte[] secret = randomHash;
|
||||
final ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH;
|
||||
final byte[] digest = randomHash;
|
||||
final byte[] metadataHash = null;
|
||||
final List<PaymentData> payments = new ArrayList<>();
|
||||
final int validService = Service.IMAGE.value;
|
||||
final int invalidService = 99999999;
|
||||
|
||||
// Try with valid service
|
||||
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
|
||||
version, validService, nonce, size, name, identifier, method,
|
||||
secret, compression, digest, dataType, metadataHash, payments);
|
||||
assertEquals(Service.IMAGE, transactionData.getService());
|
||||
|
||||
// Try with invalid service
|
||||
transactionData = new ArbitraryTransactionData(baseTransactionData,
|
||||
version, invalidService, nonce, size, name, identifier, method,
|
||||
secret, compression, digest, dataType, metadataHash, payments);
|
||||
assertNull(transactionData.getService());
|
||||
}
|
||||
}
|
||||
|
@@ -29,19 +29,22 @@ public class ArbitraryUtils {
|
||||
int chunkSize) throws DataException {
|
||||
|
||||
return ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, method, service,
|
||||
account, chunkSize, null, null, null, null);
|
||||
account, chunkSize, 0L, true, null, null, null, null);
|
||||
}
|
||||
|
||||
public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier,
|
||||
ArbitraryTransactionData.Method method, Service service, PrivateKeyAccount account,
|
||||
int chunkSize, String title, String description, List<String> tags, Category category) throws DataException {
|
||||
int chunkSize, long fee, boolean computeNonce,
|
||||
String title, String description, List<String> tags, Category category) throws DataException {
|
||||
|
||||
ArbitraryDataTransactionBuilder txnBuilder = new ArbitraryDataTransactionBuilder(
|
||||
repository, publicKey58, path, name, method, service, identifier, title, description, tags, category);
|
||||
repository, publicKey58, fee, path, name, method, service, identifier, title, description, tags, category);
|
||||
|
||||
txnBuilder.setChunkSize(chunkSize);
|
||||
txnBuilder.build();
|
||||
txnBuilder.computeNonce();
|
||||
if (computeNonce) {
|
||||
txnBuilder.computeNonce();
|
||||
}
|
||||
ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData();
|
||||
Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, account);
|
||||
assertEquals(Transaction.ValidationResult.OK, result);
|
||||
|
@@ -45,7 +45,7 @@ public class ArbitraryTestTransaction extends TestTransaction {
|
||||
List<PaymentData> payments = new ArrayList<>();
|
||||
payments.add(new PaymentData(recipient, assetId, amount));
|
||||
|
||||
return new ArbitraryTransactionData(generateBase(account), version, service, nonce, size,name, identifier,
|
||||
return new ArbitraryTransactionData(generateBase(account), version, service.value, nonce, size,name, identifier,
|
||||
method, secret, compression, data, dataType, metadataHash, payments);
|
||||
}
|
||||
|
||||
|
@@ -128,7 +128,7 @@ public class IntegrityTests extends Common {
|
||||
|
||||
// Run the database integrity check for the initial name, to ensure it doesn't get into a loop
|
||||
NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
assertEquals(2, integrityCheck.rebuildName(initialName, repository));
|
||||
assertEquals(4, integrityCheck.rebuildName(initialName, repository)); // 4 transactions total
|
||||
|
||||
// Ensure the new name still exists and the data is still correct
|
||||
assertTrue(repository.getNameRepository().nameExists(initialName));
|
||||
|
@@ -219,6 +219,65 @@ public class UpdateTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
// Test that multiple UPDATE_NAME transactions work as expected, when using a matching name and newName string
|
||||
@Test
|
||||
public void testDoubleUpdateNameWithMatchingNewName() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "name";
|
||||
String reducedName = "name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
initialTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(initialTransactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||
|
||||
// Check name exists
|
||||
assertTrue(repository.getNameRepository().nameExists(name));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(reducedName));
|
||||
|
||||
// Update name
|
||||
TransactionData middleTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, name, data);
|
||||
TransactionUtils.signAndMint(repository, middleTransactionData, alice);
|
||||
|
||||
// Check name still exists
|
||||
assertTrue(repository.getNameRepository().nameExists(name));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(reducedName));
|
||||
|
||||
// Update name again
|
||||
TransactionData newestTransactionData = new UpdateNameTransactionData(TestTransaction.generateBase(alice), name, name, data);
|
||||
TransactionUtils.signAndMint(repository, newestTransactionData, alice);
|
||||
|
||||
// Check name still exists
|
||||
assertTrue(repository.getNameRepository().nameExists(name));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(reducedName));
|
||||
|
||||
// Check updated timestamp is correct
|
||||
assertEquals((Long) newestTransactionData.getTimestamp(), repository.getNameRepository().fromName(name).getUpdated());
|
||||
|
||||
// orphan and recheck
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check name still exists
|
||||
assertTrue(repository.getNameRepository().nameExists(name));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(reducedName));
|
||||
|
||||
// Check updated timestamp is correct
|
||||
assertEquals((Long) middleTransactionData.getTimestamp(), repository.getNameRepository().fromName(name).getUpdated());
|
||||
|
||||
// orphan and recheck
|
||||
BlockUtils.orphanLastBlock(repository);
|
||||
|
||||
// Check name still exists
|
||||
assertTrue(repository.getNameRepository().nameExists(name));
|
||||
assertNotNull(repository.getNameRepository().fromReducedName(reducedName));
|
||||
|
||||
// Check updated timestamp is empty
|
||||
assertNull(repository.getNameRepository().fromName(name).getUpdated());
|
||||
}
|
||||
}
|
||||
|
||||
// Test that reverting using previous UPDATE_NAME works as expected
|
||||
@Test
|
||||
public void testIntermediateUpdateName() throws DataException {
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -78,7 +78,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -74,12 +74,13 @@
|
||||
"calcChainWeightTimestamp": 0,
|
||||
"transactionV5Timestamp": 0,
|
||||
"transactionV6Timestamp": 0,
|
||||
"disableReferenceTimestamp": 9999999999999,
|
||||
"disableReferenceTimestamp": 0,
|
||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 9999999999999
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -80,7 +80,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 20,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -79,7 +79,8 @@
|
||||
"onlineAccountMinterLevelValidationHeight": 0,
|
||||
"selfSponsorshipAlgoV1Height": 999999999,
|
||||
"feeValidationFixTimestamp": 0,
|
||||
"chatReferenceTimestamp": 0
|
||||
"chatReferenceTimestamp": 0,
|
||||
"arbitraryOptionalFeeTimestamp": 0
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
@@ -9,5 +9,6 @@
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0,
|
||||
"pruneBlockLimit": 100,
|
||||
"repositoryPath": "dbtest"
|
||||
"repositoryPath": "dbtest",
|
||||
"defaultArchiveVersion": 1
|
||||
}
|
||||
|
@@ -2,9 +2,10 @@
|
||||
|
||||
## Create testnet blockchain config
|
||||
|
||||
- You can begin by copying the mainnet blockchain config `src/main/resources/blockchain.json`
|
||||
- The simplest option is to use the testchain.json included in this folder.
|
||||
- Alternatively, you can create one by copying the mainnet blockchain config `src/main/resources/blockchain.json`
|
||||
- Insert `"isTestChain": true,` after the opening `{`
|
||||
- Modify testnet genesis block
|
||||
- Modify testnet genesis block, feature triggers etc
|
||||
|
||||
### Testnet genesis block
|
||||
|
||||
@@ -97,6 +98,10 @@ Your options are:
|
||||
{
|
||||
"isTestNet": true,
|
||||
"bitcoinNet": "TEST3",
|
||||
"litecoinNet": "TEST3",
|
||||
"dogecoinNet": "TEST3",
|
||||
"digibyteNet": "TEST3",
|
||||
"ravencoinNet": "TEST3",
|
||||
"repositoryPath": "db-testnet",
|
||||
"blockchainConfig": "testchain.json",
|
||||
"minBlockchainPeers": 1,
|
||||
@@ -112,7 +117,7 @@ Your options are:
|
||||
|
||||
## Quick start
|
||||
Here are some steps to quickly get a single node testnet up and running with a generic minting account:
|
||||
1. Start with template `settings-test.json`, and create a `testchain.json` based on mainnet's blockchain.json (or obtain one from Qortal developers). These should be in the same directory as the jar.
|
||||
1. Start with template `settings-test.json`, and `testchain.json` which can be found in this folder. Copy/move them to the same directory as the jar.
|
||||
2. Make sure feature triggers and other timestamp/height activations are correctly set. Generally these would be `0` so that they are enabled from the start.
|
||||
3. Set a recent genesis `timestamp` in testchain.json, and add this reward share entry:
|
||||
`{ "type": "REWARD_SHARE", "minterPublicKey": "DwcUnhxjamqppgfXCLgbYRx8H9XFPUc2qYRy3CEvQWEw", "recipient": "QbTDMss7NtRxxQaSqBZtSLSNdSYgvGaqFf", "rewardSharePublicKey": "CRvQXxFfUMfr4q3o1PcUZPA4aPCiubBsXkk47GzRo754", "sharePercent": 0 },`
|
18
testnet/settings-test.json
Executable file
18
testnet/settings-test.json
Executable file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"isTestNet": true,
|
||||
"bitcoinNet": "TEST3",
|
||||
"litecoinNet": "TEST3",
|
||||
"dogecoinNet": "TEST3",
|
||||
"digibyteNet": "TEST3",
|
||||
"ravencoinNet": "TEST3",
|
||||
"repositoryPath": "db-testnet",
|
||||
"blockchainConfig": "testchain.json",
|
||||
"minBlockchainPeers": 1,
|
||||
"apiDocumentationEnabled": true,
|
||||
"apiRestricted": false,
|
||||
"bootstrap": false,
|
||||
"maxPeerConnectionTime": 999999999,
|
||||
"localAuthBypassEnabled": true,
|
||||
"singleNodeTestnet": false,
|
||||
"recoveryModeTimeout": 0
|
||||
}
|
2661
testnet/testchain.json
Normal file
2661
testnet/testchain.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,11 @@ if [ -z "$*" ]; then
|
||||
echo "Usage:"
|
||||
echo
|
||||
echo "Host/update data:"
|
||||
echo "qdata POST [service] [name] PATH [dirpath] <identifier>"
|
||||
echo "qdata POST [service] [name] STRING [data-string] <identifier>"
|
||||
echo "qdn POST [service] [name] PATH [dirpath] <identifier>"
|
||||
echo "qdn POST [service] [name] STRING [data-string] <identifier>"
|
||||
echo
|
||||
echo "Fetch data:"
|
||||
echo "qdata GET [service] [name] <identifier-or-default> <filepath-or-default> <rebuild>"
|
||||
echo "qdn GET [service] [name] <identifier-or-default> <filepath-or-default> <rebuild>"
|
||||
echo
|
||||
echo "Notes:"
|
||||
echo "- When requesting a resource, please use 'default' to indicate a file with no identifier."
|
||||
|
Reference in New Issue
Block a user