Compare commits

...

39 Commits

Author SHA1 Message Date
CalDescent
eea2884112 Bump version to 4.2.3 2023-08-12 18:59:00 +01:00
CalDescent
640a472876 Unit fee increase set to 1692118800000 2023-08-12 18:55:27 +01:00
CalDescent
e244a5f882 Removed legacy code that is no longer needed. 2023-08-12 16:09:41 +01:00
CalDescent
278dca75e8 Increase minimum fee at a future undecided timestamp. 2023-08-12 15:18:29 +01:00
CalDescent
897c44ffe8 Optimized transaction importing by using a temporary cache of unconfirmed transactions. 2023-08-12 11:14:21 +01:00
CalDescent
d9147b3af3 Cache the online accounts validation result, to speed up block minting. 2023-08-12 10:24:55 +01:00
CalDescent
9574100a08 Bump version to 4.2.2 2023-08-02 21:36:57 +01:00
CalDescent
528583fe38 Added logging relating to unconfirmed transactions. 2023-08-02 21:32:57 +01:00
CalDescent
33cfd02c49 Fixed issues in commit f5c8dfe 2023-08-02 21:13:33 +01:00
CalDescent
94d3664cb0 Bump version to 4.2.1 2023-07-31 19:30:45 +01:00
CalDescent
f5c8dfe766 Added maxTransactionsPerBlock setting (default 25) to reduce minting load on slower machines.
This is a short term limit, is well above current usage levels, and can be increased substantially in future once the block minter code has been improved.
2023-07-31 19:25:26 +01:00
CalDescent
f7e1f2fca8 Increased timeout for SEARCH_QDN_RESOURCES from 10 to 30 seconds. 2023-07-28 21:47:29 +01:00
CalDescent
811b647c88 Catch UnsupportedAddressTypeException and fall back to IPv4 binding. 2023-07-28 18:58:47 +01:00
CalDescent
3215bb638d More online accounts improvements 2023-07-22 10:44:41 +01:00
CalDescent
8ae7a1d65b Removed (Get)OnlineAccountsV1 and V2, as these are no longer used. 2023-07-21 14:28:47 +01:00
CalDescent
29dcd53002 Revert "Improved filtering of online accounts data."
This reverts commit c14fca5660.
2023-07-16 20:04:45 +01:00
CalDescent
62908f867a Bump version to 4.2.0 2023-07-16 19:09:08 +01:00
CalDescent
5f86ecafd9 Refactored developer proxy, and modified IPv6 fallback so that it only occurs on a connection failure. 2023-07-09 12:35:46 +01:00
CalDescent
fe999a11f4 Include CANCEL_SELL_NAME transactions when performing a complete rebuild of names. 2023-07-08 11:06:33 +01:00
CalDescent
c14fca5660 Improved filtering of online accounts data. 2023-07-08 11:05:14 +01:00
CalDescent
fd8d720946 Added support for group encryption in service validation. 2023-06-23 13:32:23 +01:00
CalDescent
d628b3ab2a Renamed the "usePrefix" parameter to "includeResourceIdInPrefix", and slightly modified its functionality. 2023-06-17 13:04:46 +01:00
CalDescent
5928b54a33 Added developer QDN proxy.
This allows Q-Apps and websites to be developed and tested in real time, by proxying an existing webserver such as localhost:5173 from React/Vite. The proxy adds all QDN functionality to the existing server in real time. Needs UI integration before all features can be used.
2023-06-17 13:03:29 +01:00
QuickMythril
91dfc5efd0 Merge pull request #119 from QuickMythril/upgrade-tls
Upgraded to TLSv1.3
2023-06-14 10:25:25 -04:00
QuickMythril
1343a88ee3 Merge pull request #124 from QuickMythril/translate-jp
Added Japanese translations (Credit: R M)
2023-06-02 08:01:04 -04:00
QuickMythril
7f7b02f003 Updated Japanese translations (Credit: R M) 2023-06-02 06:17:48 -04:00
QuickMythril
5650923805 Merge pull request #123 from QuickMythril/2023-05-25
Added some fee info & Q-App support for foreign coins
2023-05-31 09:14:46 -04:00
CalDescent
5fb2640a3a Bump version to 4.1.3 2023-05-30 18:05:05 +01:00
CalDescent
66c91fd365 MIN_PEER_VERSION for handshake set to 4.1.1 2023-05-30 17:42:31 +01:00
CalDescent
bfc03db6a9 Default minPeerVersion set to 4.1.2 2023-05-30 17:41:39 +01:00
QuickMythril
a4bb445f3e Added Japanese translations for review 2023-05-29 04:23:22 -04:00
QuickMythril
27afcf12bf Prepared files for Japanese translations 2023-05-29 04:07:52 -04:00
CalDescent
eda6ab5701 Fixed some failing unit tests, and ignored some failing BTC ones that have been superseded by LTC. 2023-05-26 18:01:09 +02:00
QuickMythril
13da0e8a7a Adjusted fee info to long format 2023-05-25 17:26:29 -04:00
QuickMythril
d260c0a9a9 Updated info on foreign coin fees 2023-05-25 16:35:08 -04:00
QuickMythril
655073c524 Added 2m timeout for GET_WALLET_BALANCE action 2023-05-25 04:41:03 -04:00
crowetic
c8f3b6918f Update start.sh
Added better defaults for JVM_MEMORY_ARGS and description as to how and when to uncomment the line.
2023-05-24 16:20:05 -07:00
QuickMythril
1e10bcf3b0 Merge branch 'Qortal:master' into upgrade-tls 2023-05-09 15:38:20 -04:00
QuickMythril
8f847d3689 Upgraded to TLSv1.3 2023-04-21 19:30:29 -04:00
67 changed files with 1228 additions and 600 deletions

View File

@@ -576,14 +576,15 @@ let res = await qortalRequest({
```
### Send foreign coin to address
_Requires user approval_
_Requires user approval_<br />
Note: default fees can be found [here](https://github.com/Qortal/qortal-ui/blob/master/plugins/plugins/core/qdn/browser/browser.src.js#L205-L209).
```
let res = await qortalRequest({
action: "SEND_COIN",
coin: "LTC",
destinationAddress: "LSdTvMHRm8sScqwCi6x9wzYQae8JeZhx6y",
amount: 1.00000000, // 1 LTC
fee: 0.00000020 // fee per byte
fee: 0.00000020 // Optional fee per byte (default fee used if omitted, recommended) - not used for QORT or ARRR
});
```

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>4.1.2</version>
<version>4.2.3</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>

View File

@@ -96,7 +96,7 @@ public class ApiService {
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");

View File

@@ -0,0 +1,173 @@
package org.qortal.api;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.InetAccessHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.qortal.api.resource.AnnotationPostProcessor;
import org.qortal.api.resource.ApiDefinition;
import org.qortal.network.Network;
import org.qortal.repository.DataException;
import org.qortal.settings.Settings;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.SecureRandom;
public class DevProxyService {
private static DevProxyService instance;
private final ResourceConfig config;
private Server server;
private DevProxyService() {
this.config = new ResourceConfig();
this.config.packages("org.qortal.api.proxy.resource", "org.qortal.api.resource");
this.config.register(OpenApiResource.class);
this.config.register(ApiDefinition.class);
this.config.register(AnnotationPostProcessor.class);
}
public static DevProxyService getInstance() {
if (instance == null)
instance = new DevProxyService();
return instance;
}
public Iterable<Class<?>> getResources() {
return this.config.getClasses();
}
public void start() throws DataException {
try {
// Create API server
// SSL support if requested
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
if (keystorePathname != null && keystorePassword != null) {
// SSL version
if (!Files.isReadable(Path.of(keystorePathname)))
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
keyStore.load(keystoreStream, keystorePassword.toCharArray());
}
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setSslContext(sslContext);
this.server = new Server();
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(Settings.getInstance().getDevProxyPort());
SecureRequestCustomizer src = new SecureRequestCustomizer();
httpConfig.addCustomizer(src);
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
new DetectorConnectionFactory(sslConnectionFactory),
httpConnectionFactory);
portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
portUnifiedConnector.setPort(Settings.getInstance().getDevProxyPort());
this.server.addConnector(portUnifiedConnector);
} else {
// Non-SSL
InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDevProxyPort());
this.server = new Server(endpoint);
}
// Error handler
ErrorHandler errorHandler = new ApiErrorHandler();
this.server.setErrorHandler(errorHandler);
// Request logging
if (Settings.getInstance().isDevProxyLoggingEnabled()) {
RequestLogWriter logWriter = new RequestLogWriter("devproxy-requests.log");
logWriter.setAppend(true);
logWriter.setTimeZone("UTC");
RequestLog requestLog = new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT);
this.server.setRequestLog(requestLog);
}
// Access handler (currently no whitelist is used)
InetAccessHandler accessHandler = new InetAccessHandler();
this.server.setHandler(accessHandler);
// URL rewriting
RewriteHandler rewriteHandler = new RewriteHandler();
accessHandler.setHandler(rewriteHandler);
// Context
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
rewriteHandler.setHandler(context);
// Cross-origin resource sharing
FilterHolder corsFilterHolder = new FilterHolder(CrossOriginFilter.class);
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");
corsFilterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET, POST, DELETE");
corsFilterHolder.setInitParameter(CrossOriginFilter.CHAIN_PREFLIGHT_PARAM, "false");
context.addFilter(corsFilterHolder, "/*", null);
// API servlet
ServletContainer container = new ServletContainer(this.config);
ServletHolder apiServlet = new ServletHolder(container);
apiServlet.setInitOrder(1);
context.addServlet(apiServlet, "/*");
// Start server
this.server.start();
} catch (Exception e) {
// Failed to start
throw new DataException("Failed to start developer proxy", e);
}
}
public void stop() {
try {
// Stop server
this.server.stop();
} catch (Exception e) {
// Failed to stop
}
this.server = null;
instance = null;
}
}

View File

@@ -69,7 +69,7 @@ public class DomainMapService {
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");

View File

@@ -69,7 +69,7 @@ public class GatewayService {
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");

View File

@@ -24,11 +24,11 @@ public class HTMLParser {
private String theme;
private boolean usingCustomRouting;
public HTMLParser(String resourceId, String inPath, String prefix, boolean usePrefix, byte[] data,
public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data,
String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting) {
String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : "";
this.qdnBase = usePrefix ? String.format("%s/%s", prefix, resourceId) : "";
this.qdnBaseWithPath = usePrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : "";
String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath);
this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix;
this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename);
this.data = data;
this.qdnContext = qdnContext;
this.resourceId = resourceId;
@@ -82,7 +82,7 @@ public class HTMLParser {
}
public static boolean isHtmlFile(String path) {
if (path.endsWith(".html") || path.endsWith(".htm")) {
if (path.endsWith(".html") || path.endsWith(".htm") || path.equals("")) {
return true;
}
return false;

View File

@@ -48,10 +48,10 @@ public class DomainMapResource {
}
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
String inPath, String secret58, String prefix, boolean usePrefix, boolean async) {
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async) {
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
secret58, prefix, usePrefix, async, "domainMap", request, response, context);
secret58, prefix, includeResourceIdInPrefix, async, "domainMap", request, response, context);
return renderer.render();
}

View File

@@ -90,7 +90,7 @@ public class GatewayResource {
}
private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean usePrefix, boolean async) {
private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean includeResourceIdInPrefix, boolean async) {
if (inPath == null || inPath.equals("")) {
// Assume not a real file
@@ -157,7 +157,7 @@ public class GatewayResource {
}
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(name, ResourceIdType.NAME, service, identifier, outPath,
secret58, prefix, usePrefix, async, qdnContext, request, response, context);
secret58, prefix, includeResourceIdInPrefix, async, qdnContext, request, response, context);
return renderer.render();
}

View File

@@ -0,0 +1,164 @@
package org.qortal.api.proxy.resource;
import org.qortal.api.ApiError;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.HTMLParser;
import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.DevProxyManager;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.util.Enumeration;
@Path("/")
public class DevProxyServerResource {
@Context HttpServletRequest request;
@Context HttpServletResponse response;
@Context ServletContext context;
@GET
public HttpServletResponse getProxyIndex() {
return this.proxy("/");
}
@GET
@Path("{path:.*}")
public HttpServletResponse getProxyPath(@PathParam("path") String inPath) {
return this.proxy(inPath);
}
private HttpServletResponse proxy(String inPath) {
try {
String source = DevProxyManager.getInstance().getSourceHostAndPort();
if (!inPath.startsWith("/")) {
inPath = "/" + inPath;
}
String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : "";
// Open URL
URL url = new URL(String.format("http://%s%s%s", source, inPath, queryString));
HttpURLConnection con = (HttpURLConnection) url.openConnection();
// Proxy the request data
this.proxyRequestToConnection(request, con);
try {
// Make the request and proxy the response code
response.setStatus(con.getResponseCode());
}
catch (ConnectException e) {
// Tey converting localhost / 127.0.0.1 to IPv6 [::1]
if (source.startsWith("localhost") || source.startsWith("127.0.0.1")) {
int port = 80;
String[] parts = source.split(":");
if (parts.length > 1) {
port = Integer.parseInt(parts[1]);
}
source = String.format("[::1]:%d", port);
}
// Retry connection
url = new URL(String.format("http://%s%s%s", source, inPath, queryString));
con = (HttpURLConnection) url.openConnection();
this.proxyRequestToConnection(request, con);
response.setStatus(con.getResponseCode());
}
// Proxy the response data back to the caller
this.proxyConnectionToResponse(con, response, inPath);
} catch (IOException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, e.getMessage());
}
return response;
}
private void proxyRequestToConnection(HttpServletRequest request, HttpURLConnection con) throws ProtocolException {
// Proxy the request method
con.setRequestMethod(request.getMethod());
// Proxy the request headers
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
con.setRequestProperty(headerName, headerValue);
}
// TODO: proxy any POST parameters from "request" to "con"
}
private void proxyConnectionToResponse(HttpURLConnection con, HttpServletResponse response, String inPath) throws IOException {
// Proxy the response headers
for (int i = 0; ; i++) {
String headerKey = con.getHeaderFieldKey(i);
String headerValue = con.getHeaderField(i);
if (headerKey != null && headerValue != null) {
response.addHeader(headerKey, headerValue);
continue;
}
break;
}
// Read the response body
InputStream inputStream = con.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
byte[] data = outputStream.toByteArray(); // TODO: limit file size that can be read into memory
// Close the streams
outputStream.close();
inputStream.close();
// Extract filename
String filename = "";
if (inPath.contains("/")) {
String[] parts = inPath.split("/");
if (parts.length > 0) {
filename = parts[parts.length - 1];
}
}
// Parse and modify output if needed
if (HTMLParser.isHtmlFile(filename)) {
// HTML file - needs to be parsed
HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, "light", true);
htmlParser.addAdditionalHeaderTags();
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;");
response.setContentType(con.getContentType());
response.setContentLength(htmlParser.getData().length);
response.getOutputStream().write(htmlParser.getData());
}
else {
// Regular file - can be streamed directly
response.addHeader("Content-Security-Policy", "default-src 'self'");
response.setContentType(con.getContentType());
response.setContentLength(data.length);
response.getOutputStream().write(data);
}
}
}

View File

@@ -0,0 +1,96 @@
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.controller.DevProxyManager;
import org.qortal.repository.DataException;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@Path("/developer")
@Tag(name = "Developer Tools")
public class DeveloperResource {
@Context HttpServletRequest request;
@Context HttpServletResponse response;
@Context ServletContext context;
@POST
@Path("/proxy/start")
@Operation(
summary = "Start proxy server, for real time QDN app/website development",
requestBody = @RequestBody(
description = "Host and port of source webserver to be proxied",
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
example = "127.0.0.1:5173"
)
)
),
responses = {
@ApiResponse(
description = "Port number of running server",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA})
public Integer startProxy(String sourceHostAndPort) {
// TODO: API key
DevProxyManager devProxyManager = DevProxyManager.getInstance();
try {
devProxyManager.setSourceHostAndPort(sourceHostAndPort);
devProxyManager.start();
return devProxyManager.getPort();
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, e.getMessage());
}
}
@POST
@Path("/proxy/stop")
@Operation(
summary = "Stop proxy server",
responses = {
@ApiResponse(
description = "true if stopped",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "boolean"
)
)
)
}
)
public boolean stopProxy() {
DevProxyManager devProxyManager = DevProxyManager.getInstance();
devProxyManager.stop();
return !devProxyManager.isRunning();
}
}

View File

@@ -157,10 +157,10 @@ public class RenderResource {
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
String inPath, String secret58, String prefix, boolean usePrefix, boolean async, String theme) {
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme) {
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
secret58, prefix, usePrefix, async, "render", request, response, context);
secret58, prefix, includeResourceIdInPrefix, async, "render", request, response, context);
if (theme != null) {
renderer.setTheme(theme);

View File

@@ -40,7 +40,7 @@ public class ArbitraryDataRenderer {
private String inPath;
private final String secret58;
private final String prefix;
private final boolean usePrefix;
private final boolean includeResourceIdInPrefix;
private final boolean async;
private final String qdnContext;
private final HttpServletRequest request;
@@ -48,7 +48,7 @@ public class ArbitraryDataRenderer {
private final ServletContext context;
public ArbitraryDataRenderer(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
String inPath, String secret58, String prefix, boolean usePrefix, boolean async, String qdnContext,
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String qdnContext,
HttpServletRequest request, HttpServletResponse response, ServletContext context) {
this.resourceId = resourceId;
@@ -58,7 +58,7 @@ public class ArbitraryDataRenderer {
this.inPath = inPath;
this.secret58 = secret58;
this.prefix = prefix;
this.usePrefix = usePrefix;
this.includeResourceIdInPrefix = includeResourceIdInPrefix;
this.async = async;
this.qdnContext = qdnContext;
this.request = request;
@@ -159,7 +159,7 @@ public class ArbitraryDataRenderer {
if (HTMLParser.isHtmlFile(filename)) {
// HTML file - needs to be parsed
byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data, qdnContext, service, identifier, theme, usingCustomRouting);
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting);
htmlParser.addAdditionalHeaderTags();
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:;");
response.setContentType(context.getMimeType(filename));

View File

@@ -186,6 +186,7 @@ public enum Service {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String encryptedDataPrefix = "qortalEncryptedData";
private static final String encryptedGroupDataPrefix = "qortalGroupEncryptedData";
Service(int value, boolean requiresValidation, Long maxSize, boolean single, boolean isPrivate, List<String> requiredKeys) {
this.value = value;
@@ -221,10 +222,10 @@ public enum Service {
// Validate private data for single file resources
if (this.single) {
String dataString = new String(data, StandardCharsets.UTF_8);
if (this.isPrivate && !dataString.startsWith(encryptedDataPrefix)) {
if (this.isPrivate && !dataString.startsWith(encryptedDataPrefix) && !dataString.startsWith(encryptedGroupDataPrefix)) {
return ValidationResult.DATA_NOT_ENCRYPTED;
}
if (!this.isPrivate && dataString.startsWith(encryptedDataPrefix)) {
if (!this.isPrivate && (dataString.startsWith(encryptedDataPrefix) || dataString.startsWith(encryptedGroupDataPrefix))) {
return ValidationResult.DATA_ENCRYPTED;
}
}

View File

@@ -130,6 +130,9 @@ public class Block {
/** Locally-generated AT fees */
protected long ourAtFees; // Generated locally
/** Cached online accounts validation decision, to avoid revalidating when true */
private boolean onlineAccountsAlreadyValid = false;
@FunctionalInterface
private interface BlockRewardDistributor {
long distribute(long amount, Map<String, Long> balanceChanges) throws DataException;
@@ -563,6 +566,13 @@ public class Block {
}
/**
* Force online accounts to be revalidated, e.g. at final stage of block minting.
*/
public void clearOnlineAccountsValidationCache() {
this.onlineAccountsAlreadyValid = false;
}
// More information
/**
@@ -1043,6 +1053,10 @@ public class Block {
if (this.blockData.getHeight() != null && this.blockData.getHeight() == 1)
return ValidationResult.OK;
// Don't bother revalidating if accounts have already been validated in this block
if (this.onlineAccountsAlreadyValid)
return ValidationResult.OK;
// Expand block's online accounts indexes into actual accounts
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
// We use count of online accounts to validate decoded account indexes
@@ -1130,6 +1144,9 @@ public class Block {
// All online accounts valid, so save our list of online accounts for potential later use
this.cachedOnlineRewardShares = onlineRewardShares;
// Remember that the accounts are valid, to speed up subsequent checks
this.onlineAccountsAlreadyValid = true;
return ValidationResult.OK;
}

View File

@@ -48,9 +48,6 @@ public class BlockChain {
/** Transaction expiry period, starting from transaction's timestamp, in milliseconds. */
private long transactionExpiryPeriod;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long unitFee;
private int maxBytesPerUnitFee;
/** Maximum acceptable timestamp disagreement offset in milliseconds. */
@@ -89,6 +86,7 @@ public class BlockChain {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fee;
}
private List<UnitFeesByTimestamp> unitFees;
private List<UnitFeesByTimestamp> nameRegistrationUnitFees;
/** Map of which blockchain features are enabled when (height/timestamp) */
@@ -346,10 +344,6 @@ public class BlockChain {
return this.isTestChain;
}
public long getUnitFee() {
return this.unitFee;
}
public int getMaxBytesPerUnitFee() {
return this.maxBytesPerUnitFee;
}
@@ -547,13 +541,22 @@ public class BlockChain {
throw new IllegalStateException(String.format("No block timing info available for height %d", ourHeight));
}
public long getUnitFeeAtTimestamp(long ourTimestamp) {
for (int i = unitFees.size() - 1; i >= 0; --i)
if (unitFees.get(i).timestamp <= ourTimestamp)
return unitFees.get(i).fee;
// Shouldn't happen, but set a sensible default just in case
return 100000;
}
public long getNameRegistrationUnitFeeAtTimestamp(long ourTimestamp) {
for (int i = nameRegistrationUnitFees.size() - 1; i >= 0; --i)
if (nameRegistrationUnitFees.get(i).timestamp <= ourTimestamp)
return nameRegistrationUnitFees.get(i).fee;
// Default to system-wide unit fee
return this.getUnitFee();
// Shouldn't happen, but set a sensible default just in case
return 100000;
}
public int getMaxRewardSharesAtTimestamp(long ourTimestamp) {

View File

@@ -380,9 +380,13 @@ public class BlockMinter extends Thread {
parentSignatureForLastLowWeightBlock = null;
timeOfLastLowWeightBlock = null;
Long unconfirmedStartTime = NTP.getTime();
// Add unconfirmed transactions
addUnconfirmedTransactions(repository, newBlock);
LOGGER.info(String.format("Adding %d unconfirmed transactions took %d ms", newBlock.getTransactions().size(), (NTP.getTime()-unconfirmedStartTime)));
// Sign to create block's signature
newBlock.sign();
@@ -484,6 +488,9 @@ public class BlockMinter extends Thread {
// Sign to create block's signature, needed by Block.isValid()
newBlock.sign();
// User-defined limit per block
int limit = Settings.getInstance().getMaxTransactionsPerBlock();
// Attempt to add transactions until block is full, or we run out
// If a transaction makes the block invalid then skip it and it'll either expire or be in next block.
for (TransactionData transactionData : unconfirmedTransactions) {
@@ -496,6 +503,12 @@ public class BlockMinter extends Thread {
LOGGER.debug(() -> String.format("Skipping invalid transaction %s during block minting", Base58.encode(transactionData.getSignature())));
newBlock.deleteTransaction(transactionData);
}
// User-defined limit per block
List<Transaction> transactions = newBlock.getTransactions();
if (transactions != null && transactions.size() >= limit) {
break;
}
}
}
@@ -549,6 +562,9 @@ public class BlockMinter extends Thread {
// Sign to create block's signature
newBlock.sign();
// Ensure online accounts are fully re-validated in this final check
newBlock.clearOnlineAccountsValidationCache();
// Is newBlock still valid?
ValidationResult validationResult = newBlock.isValid();
if (validationResult != ValidationResult.OK)

View File

@@ -1278,13 +1278,6 @@ public class Controller extends Thread {
TransactionImporter.getInstance().onNetworkTransactionSignaturesMessage(peer, message);
break;
case GET_ONLINE_ACCOUNTS:
case ONLINE_ACCOUNTS:
case GET_ONLINE_ACCOUNTS_V2:
case ONLINE_ACCOUNTS_V2:
// No longer supported - to be eventually removed
break;
case GET_ONLINE_ACCOUNTS_V3:
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message);
break;

View File

@@ -0,0 +1,74 @@
package org.qortal.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.api.DevProxyService;
import org.qortal.repository.DataException;
import org.qortal.settings.Settings;
public class DevProxyManager {
protected static final Logger LOGGER = LogManager.getLogger(DevProxyManager.class);
private static DevProxyManager instance;
private boolean running = false;
private String sourceHostAndPort = "127.0.0.1:5173"; // Default for React/Vite
private DevProxyManager() {
}
public static DevProxyManager getInstance() {
if (instance == null)
instance = new DevProxyManager();
return instance;
}
public void start() throws DataException {
synchronized(this) {
if (this.running) {
// Already running
return;
}
LOGGER.info(String.format("Starting developer proxy service on port %d", Settings.getInstance().getDevProxyPort()));
DevProxyService devProxyService = DevProxyService.getInstance();
devProxyService.start();
this.running = true;
}
}
public void stop() {
synchronized(this) {
if (!this.running) {
// Not running
return;
}
LOGGER.info(String.format("Shutting down developer proxy service"));
DevProxyService devProxyService = DevProxyService.getInstance();
devProxyService.stop();
this.running = false;
}
}
public void setSourceHostAndPort(String sourceHostAndPort) {
this.sourceHostAndPort = sourceHostAndPort;
}
public String getSourceHostAndPort() {
return this.sourceHostAndPort;
}
public Integer getPort() {
return Settings.getInstance().getDevProxyPort();
}
public boolean isRunning() {
return this.running;
}
}

View File

@@ -414,7 +414,7 @@ public class OnlineAccountsManager {
boolean isSuperiorEntry = isOnlineAccountsDataSuperior(onlineAccountData);
if (isSuperiorEntry)
// Remove existing inferior entry so it can be re-added below (it's likely the existing copy is missing a nonce value)
onlineAccounts.remove(onlineAccountData);
onlineAccounts.removeIf(a -> Objects.equals(a.getPublicKey(), onlineAccountData.getPublicKey()));
boolean isNewEntry = onlineAccounts.add(onlineAccountData);

View File

@@ -229,13 +229,6 @@ public class Synchronizer extends Thread {
peers.removeIf(Controller.hasOldVersion);
checkRecoveryModeForPeers(peers);
if (recoveryMode) {
// Needs a mutable copy of the unmodifiableList
peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
peers.removeIf(Controller.hasOnlyGenesisBlock);
peers.removeIf(Controller.hasMisbehaved);
peers.removeIf(Controller.hasOldVersion);
}
// Check we have enough peers to potentially synchronize
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
@@ -262,10 +255,7 @@ public class Synchronizer extends Thread {
peers.removeIf(Controller.hasInferiorChainTip);
// Remove any peers that are no longer on a recent block since the last check
// Except for times when we're in recovery mode, in which case we need to keep them
if (!recoveryMode) {
peers.removeIf(Controller.hasNoRecentBlock);
}
peers.removeIf(Controller.hasNoRecentBlock);
final int peersRemoved = peersBeforeComparison - peers.size();
if (peersRemoved > 0 && peers.size() > 0)
@@ -1340,8 +1330,8 @@ public class Synchronizer extends Thread {
return SynchronizationResult.INVALID_DATA;
}
// Final check to make sure the peer isn't out of date (except for when we're in recovery mode)
if (!recoveryMode && peer.getChainTipData() != null) {
// Final check to make sure the peer isn't out of date
if (peer.getChainTipData() != null) {
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {

View File

@@ -47,6 +47,9 @@ public class TransactionImporter extends Thread {
/** Map of recent invalid unconfirmed transactions. Key is base58 transaction signature, value is do-not-request expiry timestamp. */
private final Map<String, Long> invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>());
/** Cached list of unconfirmed transactions, used when counting per creator. This is replaced regularly */
public static List<TransactionData> unconfirmedTransactionsCache = null;
public static synchronized TransactionImporter getInstance() {
if (instance == null) {
@@ -254,6 +257,12 @@ public class TransactionImporter extends Thread {
int processedCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
// Use a single copy of the unconfirmed transactions list for each cycle, to speed up constant lookups
// when counting unconfirmed transactions by creator.
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
unconfirmedTransactions.removeIf(t -> t.getType() == Transaction.TransactionType.CHAT);
unconfirmedTransactionsCache = unconfirmedTransactions;
// Import transactions with valid signatures
try {
for (int i = 0; i < sigValidTransactions.size(); ++i) {
@@ -317,6 +326,9 @@ public class TransactionImporter extends Thread {
} finally {
LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s"));
blockchainLock.unlock();
// Clear the unconfirmed transaction cache so new data can be populated in the next cycle
unconfirmedTransactionsCache = null;
}
} catch (DataException e) {
LOGGER.error("Repository issue while importing incoming transactions", e);

View File

@@ -25,7 +25,8 @@ public class NamesDatabaseIntegrityCheck {
TransactionType.REGISTER_NAME,
TransactionType.UPDATE_NAME,
TransactionType.BUY_NAME,
TransactionType.SELL_NAME
TransactionType.SELL_NAME,
TransactionType.CANCEL_SELL_NAME
);
private List<TransactionData> nameTransactions = new ArrayList<>();

View File

@@ -28,7 +28,7 @@ public abstract class TrustlessSSLSocketFactory {
private static final SSLContext sc;
static {
try {
sc = SSLContext.getInstance("SSL");
sc = SSLContext.getInstance("TLSv1.3");
sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom());
} catch (Exception e) {
throw new RuntimeException(e);

View File

@@ -1,6 +1,7 @@
package org.qortal.data.network;
import java.util.Arrays;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@@ -34,10 +35,6 @@ public class OnlineAccountData {
this.nonce = nonce;
}
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
this(timestamp, signature, publicKey, null);
}
public long getTimestamp() {
return this.timestamp;
}
@@ -76,6 +73,10 @@ public class OnlineAccountData {
if (otherOnlineAccountData.timestamp != this.timestamp)
return false;
// Almost as quick
if (!Objects.equals(otherOnlineAccountData.nonce, this.nonce))
return false;
if (!Arrays.equals(otherOnlineAccountData.publicKey, this.publicKey))
return false;
@@ -88,9 +89,10 @@ public class OnlineAccountData {
public int hashCode() {
int h = this.hash;
if (h == 0) {
this.hash = h = Long.hashCode(this.timestamp)
^ Arrays.hashCode(this.publicKey);
h = Objects.hash(timestamp, nonce);
h = 31 * h + Arrays.hashCode(publicKey);
// We don't use signature because newer aggregate signatures use random nonces
this.hash = h;
}
return h;
}

View File

@@ -265,7 +265,7 @@ public enum Handshake {
private static final long PEER_VERSION_131 = 0x0100030001L;
/** Minimum peer version that we are allowed to communicate with */
private static final String MIN_PEER_VERSION = "4.0.0";
private static final String MIN_PEER_VERSION = "4.1.1";
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits

View File

@@ -187,7 +187,7 @@ public class Network {
this.bindAddress = bindAddress; // Store the selected address, so that it can be used by other parts of the app
break; // We don't want to bind to more than one address
} catch (UnknownHostException e) {
} catch (UnknownHostException | UnsupportedAddressTypeException e) {
LOGGER.error("Can't bind listen socket to address {}", Settings.getInstance().getBindAddress());
if (i == bindAddresses.size()-1) { // Only throw an exception if all addresses have been tried
throw new IOException("Can't bind listen socket to address", e);

View File

@@ -1,69 +0,0 @@
package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.transform.Transformer;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
public class GetOnlineAccountsMessage extends Message {
private static final int MAX_ACCOUNT_COUNT = 5000;
private List<OnlineAccountData> onlineAccounts;
public GetOnlineAccountsMessage(List<OnlineAccountData> onlineAccounts) {
super(MessageType.GET_ONLINE_ACCOUNTS);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(onlineAccounts.size()));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
bytes.write(onlineAccountData.getPublicKey());
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetOnlineAccountsMessage(int id, List<OnlineAccountData> onlineAccounts) {
super(id, MessageType.GET_ONLINE_ACCOUNTS);
this.onlineAccounts = onlineAccounts.stream().limit(MAX_ACCOUNT_COUNT).collect(Collectors.toList());
}
public List<OnlineAccountData> getOnlineAccounts() {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
final int accountCount = bytes.getInt();
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
for (int i = 0; i < Math.min(MAX_ACCOUNT_COUNT, accountCount); ++i) {
long timestamp = bytes.getLong();
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
bytes.get(publicKey);
onlineAccounts.add(new OnlineAccountData(timestamp, null, publicKey));
}
return new GetOnlineAccountsMessage(id, onlineAccounts);
}
}

View File

@@ -1,109 +0,0 @@
package org.qortal.network.message;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* For requesting online accounts info from remote peer, given our list of online accounts.
*
* Different format to V1:
* V1 is: number of entries, then timestamp + pubkey for each entry
* V2 is: groups of: number of entries, timestamp, then pubkey for each entry
*
* Also V2 only builds online accounts message once!
*/
public class GetOnlineAccountsV2Message extends Message {
private List<OnlineAccountData> onlineAccounts;
public GetOnlineAccountsV2Message(List<OnlineAccountData> onlineAccounts) {
super(MessageType.GET_ONLINE_ACCOUNTS_V2);
// If we don't have ANY online accounts then it's an easier construction...
if (onlineAccounts.isEmpty()) {
// Always supply a number of accounts
this.dataBytes = Ints.toByteArray(0);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
return;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (OnlineAccountData onlineAccountData : onlineAccounts) {
Long timestamp = onlineAccountData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH;
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
try {
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
if (onlineAccountData.getTimestamp() == timestamp)
bytes.write(onlineAccountData.getPublicKey());
}
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetOnlineAccountsV2Message(int id, List<OnlineAccountData> onlineAccounts) {
super(id, MessageType.GET_ONLINE_ACCOUNTS_V2);
this.onlineAccounts = onlineAccounts;
}
public List<OnlineAccountData> getOnlineAccounts() {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int accountCount = bytes.getInt();
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
while (accountCount > 0) {
long timestamp = bytes.getLong();
for (int i = 0; i < accountCount; ++i) {
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
bytes.get(publicKey);
onlineAccounts.add(new OnlineAccountData(timestamp, null, publicKey));
}
if (bytes.hasRemaining()) {
accountCount = bytes.getInt();
} else {
// we've finished
accountCount = 0;
}
}
return new GetOnlineAccountsV2Message(id, onlineAccounts);
}
}

View File

@@ -43,11 +43,7 @@ public enum MessageType {
BLOCK_SUMMARIES(70, BlockSummariesMessage::fromByteBuffer),
GET_BLOCK_SUMMARIES(71, GetBlockSummariesMessage::fromByteBuffer),
BLOCK_SUMMARIES_V2(72, BlockSummariesV2Message::fromByteBuffer),
ONLINE_ACCOUNTS(80, OnlineAccountsMessage::fromByteBuffer),
GET_ONLINE_ACCOUNTS(81, GetOnlineAccountsMessage::fromByteBuffer),
ONLINE_ACCOUNTS_V2(82, OnlineAccountsV2Message::fromByteBuffer),
GET_ONLINE_ACCOUNTS_V2(83, GetOnlineAccountsV2Message::fromByteBuffer),
ONLINE_ACCOUNTS_V3(84, OnlineAccountsV3Message::fromByteBuffer),
GET_ONLINE_ACCOUNTS_V3(85, GetOnlineAccountsV3Message::fromByteBuffer),

View File

@@ -1,75 +0,0 @@
package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.transform.Transformer;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
public class OnlineAccountsMessage extends Message {
private static final int MAX_ACCOUNT_COUNT = 5000;
private List<OnlineAccountData> onlineAccounts;
public OnlineAccountsMessage(List<OnlineAccountData> onlineAccounts) {
super(MessageType.ONLINE_ACCOUNTS);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(onlineAccounts.size()));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
bytes.write(onlineAccountData.getSignature());
bytes.write(onlineAccountData.getPublicKey());
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private OnlineAccountsMessage(int id, List<OnlineAccountData> onlineAccounts) {
super(id, MessageType.ONLINE_ACCOUNTS);
this.onlineAccounts = onlineAccounts.stream().limit(MAX_ACCOUNT_COUNT).collect(Collectors.toList());
}
public List<OnlineAccountData> getOnlineAccounts() {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
final int accountCount = bytes.getInt();
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
for (int i = 0; i < Math.min(MAX_ACCOUNT_COUNT, accountCount); ++i) {
long timestamp = bytes.getLong();
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
bytes.get(publicKey);
OnlineAccountData onlineAccountData = new OnlineAccountData(timestamp, signature, publicKey);
onlineAccounts.add(onlineAccountData);
}
return new OnlineAccountsMessage(id, onlineAccounts);
}
}

View File

@@ -1,113 +0,0 @@
package org.qortal.network.message;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* For sending online accounts info to remote peer.
*
* Different format to V1:
* V1 is: number of entries, then timestamp + sig + pubkey for each entry
* V2 is: groups of: number of entries, timestamp, then sig + pubkey for each entry
*
* Also V2 only builds online accounts message once!
*/
public class OnlineAccountsV2Message extends Message {
private List<OnlineAccountData> onlineAccounts;
public OnlineAccountsV2Message(List<OnlineAccountData> onlineAccounts) {
super(MessageType.ONLINE_ACCOUNTS_V2);
// Shortcut in case we have no online accounts
if (onlineAccounts.isEmpty()) {
this.dataBytes = Ints.toByteArray(0);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
return;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (OnlineAccountData onlineAccountData : onlineAccounts) {
Long timestamp = onlineAccountData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
try {
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
if (onlineAccountData.getTimestamp() == timestamp) {
bytes.write(onlineAccountData.getSignature());
bytes.write(onlineAccountData.getPublicKey());
}
}
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private OnlineAccountsV2Message(int id, List<OnlineAccountData> onlineAccounts) {
super(id, MessageType.ONLINE_ACCOUNTS_V2);
this.onlineAccounts = onlineAccounts;
}
public List<OnlineAccountData> getOnlineAccounts() {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
int accountCount = bytes.getInt();
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
while (accountCount > 0) {
long timestamp = bytes.getLong();
for (int i = 0; i < accountCount; ++i) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
bytes.get(publicKey);
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey));
}
if (bytes.hasRemaining()) {
accountCount = bytes.getInt();
} else {
// we've finished
accountCount = 0;
}
}
return new OnlineAccountsV2Message(id, onlineAccounts);
}
}

View File

@@ -99,9 +99,10 @@ public class OnlineAccountsV3Message extends Message {
bytes.get(publicKey);
// Nonce is optional - will be -1 if missing
// ... but we should skip/ignore an online account if it has no nonce
Integer nonce = bytes.getInt();
if (nonce < 0) {
nonce = null;
continue;
}
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey, nonce));

View File

@@ -314,7 +314,7 @@ public interface TransactionRepository {
* @return list of transactions, or empty if none.
* @throws DataException
*/
public List<TransactionData> getUnconfirmedTransactions(EnumSet<TransactionType> excludedTxTypes) throws DataException;
public List<TransactionData> getUnconfirmedTransactions(EnumSet<TransactionType> excludedTxTypes, Integer limit) throws DataException;
/**
* Remove transaction from unconfirmed transactions pile.

View File

@@ -1429,8 +1429,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
@Override
public List<TransactionData> getUnconfirmedTransactions(EnumSet<TransactionType> excludedTxTypes) throws DataException {
public List<TransactionData> getUnconfirmedTransactions(EnumSet<TransactionType> excludedTxTypes, Integer limit) throws DataException {
StringBuilder sql = new StringBuilder(1024);
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT signature FROM UnconfirmedTransactions ");
sql.append("JOIN Transactions USING (signature) ");
sql.append("WHERE type NOT IN (");
@@ -1446,12 +1448,17 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
sql.append(")");
sql.append("ORDER BY created_when, signature");
sql.append("ORDER BY created_when, signature ");
if (limit != null) {
sql.append("LIMIT ?");
bindParams.add(limit);
}
List<TransactionData> transactions = new ArrayList<>();
// Find transactions with no corresponding row in BlockTransactions
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return transactions;

View File

@@ -47,6 +47,9 @@ public class Settings {
private static final int MAINNET_GATEWAY_PORT = 80;
private static final int TESTNET_GATEWAY_PORT = 8080;
private static final int MAINNET_DEV_PROXY_PORT = 12393;
private static final int TESTNET_DEV_PROXY_PORT = 62393;
private static final Logger LOGGER = LogManager.getLogger(Settings.class);
private static final String SETTINGS_FILENAME = "settings.json";
@@ -107,6 +110,11 @@ public class Settings {
private boolean gatewayLoggingEnabled = false;
private boolean gatewayLoopbackEnabled = false;
// Developer Proxy
private Integer devProxyPort;
private boolean devProxyLoggingEnabled = false;
// Specific to this node
private boolean wipeUnconfirmedOnStart = false;
/** Maximum number of unconfirmed transactions allowed per account */
@@ -138,6 +146,9 @@ public class Settings {
/* How many blocks to cache locally. Defaulted to 10, which covers a typical Synchronizer request + a few spare */
private int blockCacheSize = 10;
/** Maximum number of transactions for the block minter to include in a block */
private int maxTransactionsPerBlock = 25;
/** How long to keep old, full, AT state data (ms). */
private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds
/** How often to attempt AT state trimming (ms). */
@@ -216,10 +227,10 @@ public class Settings {
private int maxRetries = 2;
/** The number of seconds of no activity before recovery mode begins */
public long recoveryModeTimeout = 24 * 60 * 60 * 1000L;
public long recoveryModeTimeout = 9999999999999L;
/** Minimum peer version number required in order to sync with them */
private String minPeerVersion = "4.1.1";
private String minPeerVersion = "4.1.2";
/** 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 */
@@ -649,6 +660,18 @@ public class Settings {
}
public int getDevProxyPort() {
if (this.devProxyPort != null)
return this.devProxyPort;
return this.isTestNet ? TESTNET_DEV_PROXY_PORT : MAINNET_DEV_PROXY_PORT;
}
public boolean isDevProxyLoggingEnabled() {
return this.devProxyLoggingEnabled;
}
public boolean getWipeUnconfirmedOnStart() {
return this.wipeUnconfirmedOnStart;
}
@@ -673,6 +696,10 @@ public class Settings {
return this.blockCacheSize;
}
public int getMaxTransactionsPerBlock() {
return this.maxTransactionsPerBlock;
}
public boolean isTestNet() {
return this.isTestNet;
}

View File

@@ -13,6 +13,7 @@ import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.controller.Controller;
import org.qortal.controller.TransactionImporter;
import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData;
import org.qortal.data.group.GroupApprovalData;
@@ -377,7 +378,7 @@ public abstract class Transaction {
* @return
*/
public long getUnitFee(Long timestamp) {
return BlockChain.getInstance().getUnitFee();
return BlockChain.getInstance().getUnitFeeAtTimestamp(timestamp);
}
/**
@@ -617,7 +618,10 @@ public abstract class Transaction {
}
private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException {
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
List<TransactionData> unconfirmedTransactions = TransactionImporter.getInstance().unconfirmedTransactionsCache;
if (unconfirmedTransactions == null) {
unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
}
// We exclude CHAT transactions as they never get included into blocks and
// have spam/DoS prevention by requiring proof of work
@@ -641,7 +645,7 @@ public abstract class Transaction {
BlockData latestBlockData = repository.getBlockRepository().getLastBlock();
EnumSet<TransactionType> excludedTxTypes = EnumSet.of(TransactionType.CHAT, TransactionType.PRESENCE);
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions(excludedTxTypes);
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions(excludedTxTypes, null);
unconfirmedTransactions.sort(getDataComparator());

View File

@@ -3,8 +3,12 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.001",
"unitFees": [
{ "timestamp": 0, "fee": "0.001" },
{ "timestamp": 1692118800000, "fee": "0.01" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.001" },
{ "timestamp": 1645372800000, "fee": "5" },
{ "timestamp": 1651420800000, "fee": "1.25" }
],

View File

@@ -0,0 +1,83 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# Keys are from api.ApiError enum
# "localeLang": "jp",
### Common ###
JSON = JSON メッセージの解析に失敗しました
INSUFFICIENT_BALANCE = 残高不足
UNAUTHORIZED = APIコール未承認
REPOSITORY_ISSUE = リポジトリエラー
NON_PRODUCTION = この APIコールはプロダクションシステムでは許可されていません
BLOCKCHAIN_NEEDS_SYNC = ブロックチェーンをまず同期する必要があります
NO_TIME_SYNC = 時刻が未同期
### Validation ###
INVALID_SIGNATURE = 無効な署名
INVALID_ADDRESS = 無効なアドレス
INVALID_PUBLIC_KEY = 無効な公開鍵
INVALID_DATA = 無効なデータ
INVALID_NETWORK_ADDRESS = 無効なネットワーク アドレス
ADDRESS_UNKNOWN = 不明なアカウントアドレス
INVALID_CRITERIA = 無効な検索条件
INVALID_REFERENCE = 無効な参照
TRANSFORMATION_ERROR = JSONをトランザクションに変換出来ませんでした
INVALID_PRIVATE_KEY = 無効な秘密鍵
INVALID_HEIGHT = 無効なブロック高
CANNOT_MINT = アカウントはミント出来ません
### Blocks ###
BLOCK_UNKNOWN = 不明なブロック
### Transactions ###
TRANSACTION_UNKNOWN = 不明なトランザクション
PUBLIC_KEY_NOT_FOUND = 公開鍵が見つかりません
# this one is special in that caller expected to pass two additional strings, hence the two %s
TRANSACTION_INVALID = 無効なトランザクション: %s (%s)
### Naming ###
NAME_UNKNOWN = 不明な名前
### Asset ###
INVALID_ASSET_ID = 無効なアセット ID
INVALID_ORDER_ID = 無効なアセット注文 ID
ORDER_UNKNOWN = 不明なアセット注文 ID
### Groups ###
GROUP_UNKNOWN = 不明なグループ
### Foreign Blockchain ###
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = 外部ブロックチェーンまたはElectrumXネットワークの問題
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = 外部ブロックチェーンの残高が不足しています
FOREIGN_BLOCKCHAIN_TOO_SOON = 外部ブロックチェーン トランザクションのブロードキャストが時期尚早 (ロックタイム/ブロック時間の中央値)
### Trade Portal ###
ORDER_SIZE_TOO_SMALL = 注文金額が低すぎます
### Data ###
FILE_NOT_FOUND = ファイルが見つかりません
NO_REPLY = ピアが制限時間内に応答しませんでした

View File

@@ -0,0 +1,48 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu # Japanese translation by R M 2023
APPLYING_UPDATE_AND_RESTARTING = 自動更新を適用して再起動しています...
AUTO_UPDATE = 自動更新
BLOCK_HEIGHT = ブロック高
BLOCKS_REMAINING = 残りのブロック
BUILD_VERSION = ビルドバージョン
CHECK_TIME_ACCURACY = 時刻の精度を確認
CONNECTING = 接続中
CONNECTION = 接続
CONNECTIONS = 接続
CREATING_BACKUP_OF_DB_FILES = データベース ファイルのバックアップを作成中...
DB_BACKUP = データベースのバックアップ
DB_CHECKPOINT = データベースのチェックポイント
DB_MAINTENANCE = データベースのメンテナンス
EXIT = 終了
LITE_NODE = ライトノード
MINTING_DISABLED = ミント一時中止中
MINTING_ENABLED = \u2714 ミント
OPEN_UI = UIを開く
PERFORMING_DB_CHECKPOINT = コミットされていないデータベースの変更を保存中...
PERFORMING_DB_MAINTENANCE = 定期メンテナンスを実行中...
SYNCHRONIZE_CLOCK = 時刻を同期
SYNCHRONIZING_BLOCKCHAIN = ブロックチェーンを同期中
SYNCHRONIZING_CLOCK = 時刻を同期中

View File

@@ -0,0 +1,195 @@
#
ACCOUNT_ALREADY_EXISTS = 既にアカウントは存在します
ACCOUNT_CANNOT_REWARD_SHARE = アカウントは報酬シェアが出来ません
ADDRESS_ABOVE_RATE_LIMIT = アドレスが指定されたレート制限に達しました
ADDRESS_BLOCKED = このアドレスはブロックされています
ALREADY_GROUP_ADMIN = 既ににグループ管理者です
ALREADY_GROUP_MEMBER = 既にグループメンバーです
ALREADY_VOTED_FOR_THAT_OPTION = 既にそのオプションに投票しています
ASSET_ALREADY_EXISTS = 既にアセットは存在します
ASSET_DOES_NOT_EXIST = アセットが存在しません
ASSET_DOES_NOT_MATCH_AT = アセットがATのアセットと一致しません
ASSET_NOT_SPENDABLE = 資産が使用不可です
AT_ALREADY_EXISTS = 既にATが存在します
AT_IS_FINISHED = ATが終了しました
AT_UNKNOWN = 不明なAT
BAN_EXISTS = 既にバンされてます
BAN_UNKNOWN = 不明なバン
BANNED_FROM_GROUP = グループからのバンされています
BUYER_ALREADY_OWNER = 既に購入者が所有者です
CLOCK_NOT_SYNCED = 時刻が未同期
DUPLICATE_MESSAGE = このアドレスは重複メッセージを送信しました
DUPLICATE_OPTION = 重複したオプション
GROUP_ALREADY_EXISTS = 既にグループは存在します
GROUP_APPROVAL_DECIDED = 既にグループの承認は決定されています
GROUP_APPROVAL_NOT_REQUIRED = グループ承認が不必要
GROUP_DOES_NOT_EXIST = グループが存在しません
GROUP_ID_MISMATCH = グループ ID が不一致
GROUP_OWNER_CANNOT_LEAVE = グループ所有者はグループを退会出来ません
HAVE_EQUALS_WANT = 持っている資産は欲しい資産と同じです
INCORRECT_NONCE = 不正な PoW ナンス
INSUFFICIENT_FEE = 手数料が不十分です
INVALID_ADDRESS = 無効なアドレス
INVALID_AMOUNT = 無効な金額
INVALID_ASSET_OWNER = 無効なアセット所有者
INVALID_AT_TRANSACTION = 無効なATトランザクション
INVALID_AT_TYPE_LENGTH = 無効なATの「タイプ」の長さです
INVALID_BUT_OK = 無効だがOK
INVALID_CREATION_BYTES = 無効な作成バイト数
INVALID_DATA_LENGTH = 無効なデータ長
INVALID_DESCRIPTION_LENGTH = 無効な概要の長さ
INVALID_GROUP_APPROVAL_THRESHOLD = 無効なグループ承認のしきい値
INVALID_GROUP_BLOCK_DELAY = 無効なグループ承認のブロック遅延
INVALID_GROUP_ID = 無効なグループ ID
INVALID_GROUP_OWNER = 無効なグループ所有者
INVALID_LIFETIME = 無効な有効期間
INVALID_NAME_LENGTH = 無効な名前の長さです
INVALID_NAME_OWNER = 無効な名前の所有者
INVALID_OPTION_LENGTH = 無効なオプションの長さ
INVALID_OPTIONS_COUNT = 無効なオプションの数
INVALID_ORDER_CREATOR = 無効な注文作成者
INVALID_PAYMENTS_COUNT = 無効な入出金数
INVALID_PUBLIC_KEY = 無効な公開鍵
INVALID_QUANTITY = 無効な数量
INVALID_REFERENCE = 無効な参照
INVALID_RETURN = 無効な返品
INVALID_REWARD_SHARE_PERCENT = 無効な報酬シェア率
INVALID_SELLER = 無効な販売者
INVALID_TAGS_LENGTH = 無効な「タグ」の長さ
INVALID_TIMESTAMP_SIGNATURE = 無効なタイムスタンプ署名
INVALID_TX_GROUP_ID = 無効なトランザクション グループ ID
INVALID_VALUE_LENGTH = 無効な「値」の長さ
INVITE_UNKNOWN = 不明なグループ招待
JOIN_REQUEST_EXISTS = 既にグループ参加リクエストが存在します
MAXIMUM_REWARD_SHARES = 既にこのアカウントの報酬シェアは最大です
MISSING_CREATOR = 作成者が見つかりません
MULTIPLE_NAMES_FORBIDDEN = アカウントごとに複数の登録名は禁止されています
NAME_ALREADY_FOR_SALE = 既に名前は販売中です
NAME_ALREADY_REGISTERED = 既に名前は登録されています
NAME_BLOCKED = この名前はブロックされています
NAME_DOES_NOT_EXIST = 名前は存在しません
NAME_NOT_FOR_SALE = 名前は非売品です
NAME_NOT_NORMALIZED = 名前は Unicode の「正規化」形式ではありません
NEGATIVE_AMOUNT = 無効な/負の金額
NEGATIVE_FEE = 無効な/負の料金
NEGATIVE_PRICE = 無効な/負の価格
NO_BALANCE = 残高が不足しています
NO_BLOCKCHAIN_LOCK = ノードのブロックチェーンは現在ビジーです
NO_FLAG_PERMISSION = アカウントにはその権限がありません
NOT_GROUP_ADMIN = アカウントはグループ管理者ではありません
NOT_GROUP_MEMBER = アカウントはグループメンバーではありません
NOT_MINTING_ACCOUNT = アカウントはミント出来ません
NOT_YET_RELEASED = 機能はまだリリースされていません
OK = OK
ORDER_ALREADY_CLOSED = 既に資産取引注文は終了しています
ORDER_DOES_NOT_EXIST = 資産取引注文が存在しません
POLL_ALREADY_EXISTS = 既に投票は存在します
POLL_DOES_NOT_EXIST = 投票は存在しません
POLL_OPTION_DOES_NOT_EXIST = 投票オプションが存在しません
PUBLIC_KEY_UNKNOWN = 不明な公開鍵
REWARD_SHARE_UNKNOWN = 不明な報酬シェア
SELF_SHARE_EXISTS = 既に自己シェア(報酬シェア)が存在します
TIMESTAMP_TOO_NEW = タイムスタンプが新しすぎます
TIMESTAMP_TOO_OLD = タイムスタンプが古すぎます
TOO_MANY_UNCONFIRMED = アカウントに保留中の未承認トランザクションが多すぎます
TRANSACTION_ALREADY_CONFIRMED = 既にトランザクションは承認されています
TRANSACTION_ALREADY_EXISTS = 既にトランザクションは存在します
TRANSACTION_UNKNOWN = 不明なトランザクション
TX_GROUP_ID_MISMATCH = トランザクションのグループIDが一致しません

View File

@@ -448,6 +448,10 @@ function getDefaultTimeout(action) {
// User may take a long time to accept/deny the popup
return 60 * 60 * 1000;
case "SEARCH_QDN_RESOURCES":
// Searching for data can be slow, especially when metadata and statuses are also being included
return 30 * 1000;
case "FETCH_QDN_RESOURCE":
// Fetching data can take a while, especially if the status hasn't been checked first
return 60 * 1000;
@@ -467,6 +471,10 @@ function getDefaultTimeout(action) {
// Allow extra time for other actions that create transactions, even if there is no PoW
return 5 * 60 * 1000;
case "GET_WALLET_BALANCE":
// Getting a wallet balance can take a while, if there are many transactions
return 2 * 60 * 1000;
default:
break;
}

View File

@@ -212,7 +212,7 @@ public class BootstrapTests extends Common {
@Test
public void testBootstrapHosts() throws IOException {
String[] bootstrapHosts = Settings.getInstance().getBootstrapHosts();
String[] bootstrapTypes = { "archive", "toponly" };
String[] bootstrapTypes = { "archive" }; // , "toponly"
for (String host : bootstrapHosts) {
for (String type : bootstrapTypes) {

View File

@@ -85,7 +85,7 @@ public class MessageTests extends Common {
byte[] randomReference = new byte[64];
random.nextBytes(randomReference);
long minimumFee = BlockChain.getInstance().getUnitFee();
long minimumFee = BlockChain.getInstance().getUnitFeeAtTimestamp(System.currentTimeMillis());
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");

View File

@@ -456,6 +456,25 @@ public class ArbitraryServiceTests extends Common {
assertEquals(ValidationResult.OK, service.validate(filePath));
}
@Test
public void testValidPrivateGroupData() throws IOException {
String dataString = "qortalGroupEncryptedDatabMx4fELNTV+ifJxmv4+GcuOIJOTo+3qAvbWKNY2L1rfla5UBoEcoxbtjgZ9G7FLPb8V/Qfr0bfKWfvMmN06U/pgUdLuv2mGL2V0D3qYd1011MUzGdNG1qERjaCDz8GAi63+KnHHjfMtPgYt6bcqjs4CNV+ZZ4dIt3xxHYyVEBNc=";
// Write the data a single file in a temp path
Path path = Files.createTempDirectory("testValidPrivateData");
Path filePath = Paths.get(path.toString(), "test");
filePath.toFile().deleteOnExit();
BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toFile()));
writer.write(dataString);
writer.close();
Service service = Service.FILE_PRIVATE;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.OK, service.validate(filePath));
}
@Test
public void testEncryptedData() throws IOException {
String dataString = "qortalEncryptedDatabMx4fELNTV+ifJxmv4+GcuOIJOTo+3qAvbWKNY2L1rfla5UBoEcoxbtjgZ9G7FLPb8V/Qfr0bfKWfvMmN06U/pgUdLuv2mGL2V0D3qYd1011MUzGdNG1qERjaCDz8GAi63+KnHHjfMtPgYt6bcqjs4CNV+ZZ4dIt3xxHYyVEBNc=";

View File

@@ -7,13 +7,15 @@ import org.qortal.block.BlockChain;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.utils.NTP;
public abstract class TestTransaction {
protected static final Random random = new Random();
public static BaseTransactionData generateBase(PrivateKeyAccount account, int txGroupId) throws DataException {
return new BaseTransactionData(System.currentTimeMillis(), txGroupId, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null);
long timestamp = System.currentTimeMillis();
return new BaseTransactionData(timestamp, txGroupId, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFeeAtTimestamp(timestamp), null);
}
public static BaseTransactionData generateBase(PrivateKeyAccount account) throws DataException {

View File

@@ -8,6 +8,7 @@ import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException;
@@ -32,6 +33,7 @@ public class BitcoinTests extends Common {
}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
System.out.println(String.format("Starting BTC instance..."));
System.out.println(String.format("BTC instance started"));
@@ -53,6 +55,7 @@ public class BitcoinTests extends Common {
}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
@@ -65,6 +68,7 @@ public class BitcoinTests extends Common {
}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
@@ -81,6 +85,7 @@ public class BitcoinTests extends Common {
}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
@@ -102,6 +107,7 @@ public class BitcoinTests extends Common {
}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";

View File

@@ -8,6 +8,7 @@ import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
import org.qortal.crypto.Crypto;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
@@ -18,17 +19,19 @@ import com.google.common.primitives.Longs;
public class HtlcTests extends Common {
private Bitcoin bitcoin;
private Litecoin litecoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
bitcoin = Bitcoin.getInstance();
litecoin = Litecoin.getInstance();
}
@After
public void afterTest() {
Bitcoin.resetForTesting();
bitcoin = null;
litecoin = null;
}
@Test
@@ -52,12 +55,12 @@ public class HtlcTests extends Common {
do {
// We need to perform fresh setup for 1st test
Bitcoin.resetForTesting();
bitcoin = Bitcoin.getInstance();
litecoin = Litecoin.getInstance();
long now = System.currentTimeMillis();
long timestampBoundary = now / 30_000L;
byte[] secret1 = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
byte[] secret1 = BitcoinyHTLC.findHtlcSecret(litecoin, p2shAddress);
long executionPeriod1 = System.currentTimeMillis() - now;
assertNotNull(secret1);
@@ -65,7 +68,7 @@ public class HtlcTests extends Common {
assertTrue("1st execution period should not be instant!", executionPeriod1 > 10);
byte[] secret2 = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
byte[] secret2 = BitcoinyHTLC.findHtlcSecret(litecoin, p2shAddress);
long executionPeriod2 = System.currentTimeMillis() - now - executionPeriod1;
assertNotNull(secret2);
@@ -86,7 +89,7 @@ public class HtlcTests extends Common {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
BitcoinyHTLC.Status htlcStatus = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L);
BitcoinyHTLC.Status htlcStatus = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddress, 1L);
assertNotNull(htlcStatus);
System.out.println(String.format("HTLC %s status: %s", p2shAddress, htlcStatus.name()));
@@ -97,21 +100,21 @@ public class HtlcTests extends Common {
do {
// We need to perform fresh setup for 1st test
Bitcoin.resetForTesting();
bitcoin = Bitcoin.getInstance();
litecoin = Litecoin.getInstance();
long now = System.currentTimeMillis();
long timestampBoundary = now / 30_000L;
// Won't ever exist
String p2shAddress = bitcoin.deriveP2shAddress(Crypto.hash160(Longs.toByteArray(now)));
String p2shAddress = litecoin.deriveP2shAddress(Crypto.hash160(Longs.toByteArray(now)));
BitcoinyHTLC.Status htlcStatus1 = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L);
BitcoinyHTLC.Status htlcStatus1 = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddress, 1L);
long executionPeriod1 = System.currentTimeMillis() - now;
assertNotNull(htlcStatus1);
assertTrue("1st execution period should not be instant!", executionPeriod1 > 10);
BitcoinyHTLC.Status htlcStatus2 = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L);
BitcoinyHTLC.Status htlcStatus2 = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddress, 1L);
long executionPeriod2 = System.currentTimeMillis() - now - executionPeriod1;
assertNotNull(htlcStatus2);

View File

@@ -5,7 +5,6 @@ import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -33,12 +32,12 @@ public class LitecoinTests extends Common {
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
public void testGetMedianBlockTime() throws ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", litecoin.getMedianBlockTime()));
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", litecoin.getMedianBlockTime()));
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;

View File

@@ -44,7 +44,7 @@ public class BuySellTests extends Common {
bob = Common.getTestAccount(repository, "bob");
name = "test name" + " " + random.nextInt(1_000_000);
price = random.nextInt(1000) * Amounts.MULTIPLIER;
price = (random.nextInt(1000) + 1) * Amounts.MULTIPLIER;
}
@After

View File

@@ -20,6 +20,7 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.*;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.PaymentTransaction;
import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -329,15 +330,19 @@ public class MiscTests extends Common {
public void testRegisterNameFeeIncrease() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
// Set nameRegistrationUnitFeeTimestamp to a time far in the future
// Add original fee to nameRegistrationUnitFees
UnitFeesByTimestamp originalFee = new UnitFeesByTimestamp();
originalFee.timestamp = 0L;
originalFee.fee = new AmountTypeAdapter().unmarshal("0.1");
// Add a time far in the future to nameRegistrationUnitFees
UnitFeesByTimestamp futureFeeIncrease = new UnitFeesByTimestamp();
futureFeeIncrease.timestamp = 9999999999999L; // 20 Nov 2286
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("5");
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(futureFeeIncrease), true);
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(originalFee, futureFeeIncrease), true);
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
// Validate unit fees pre and post timestamp
assertEquals(10000000, BlockChain.getInstance().getUnitFee()); // 0.1 QORT
assertEquals(10000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp - 1)); // 0.1 QORT
assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); // 5 QORT
@@ -362,7 +367,7 @@ public class MiscTests extends Common {
futureFeeIncrease.timestamp = now + (60 * 60 * 1000L); // 1 hour in the future
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("10");
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(pastFeeIncrease, futureFeeIncrease), true);
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(originalFee, pastFeeIncrease, futureFeeIncrease), true);
assertEquals(pastFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(pastFeeIncrease.timestamp));
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
@@ -387,4 +392,123 @@ public class MiscTests extends Common {
}
}
// test reading the name registration fee schedule from blockchain.json / test-chain-v2.json
@Test
public void testRegisterNameFeeScheduleInTestchainData() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
final long expectedFutureFeeIncreaseTimestamp = 9999999999999L; // 20 Nov 2286, as per test-chain-v2.json
final long expectedFutureFeeIncreaseValue = new AmountTypeAdapter().unmarshal("5");
assertEquals(expectedFutureFeeIncreaseValue, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp));
// Validate unit fees pre and post timestamp
assertEquals(10000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp - 1)); // 0.1 QORT
assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp)); // 5 QORT
// Register-name
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String name = "test-name";
String data = "{\"age\":30}";
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
assertEquals(10000000L, transactionData.getFee().longValue());
TransactionUtils.signAndMint(repository, transactionData, alice);
}
}
// test general unit fee increase
@Test
public void testUnitFeeIncrease() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
// Add original fee to unitFees
UnitFeesByTimestamp originalFee = new UnitFeesByTimestamp();
originalFee.timestamp = 0L;
originalFee.fee = new AmountTypeAdapter().unmarshal("0.1");
// Add a time far in the future to unitFees
UnitFeesByTimestamp futureFeeIncrease = new UnitFeesByTimestamp();
futureFeeIncrease.timestamp = 9999999999999L; // 20 Nov 2286
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("1");
FieldUtils.writeField(BlockChain.getInstance(), "unitFees", Arrays.asList(originalFee, futureFeeIncrease), true);
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
// Validate unit fees pre and post timestamp
assertEquals(10000000, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp - 1)); // 0.1 QORT
assertEquals(100000000, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp)); // 1 QORT
// Payment
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PaymentTransactionData transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 100000);
transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
assertEquals(10000000L, transactionData.getFee().longValue());
TransactionUtils.signAndMint(repository, transactionData, alice);
// Set fee increase to a time in the past
Long now = NTP.getTime();
UnitFeesByTimestamp pastFeeIncrease = new UnitFeesByTimestamp();
pastFeeIncrease.timestamp = now - 1000L; // 1 second ago
pastFeeIncrease.fee = new AmountTypeAdapter().unmarshal("3");
// Set another increase in the future
futureFeeIncrease = new UnitFeesByTimestamp();
futureFeeIncrease.timestamp = now + (60 * 60 * 1000L); // 1 hour in the future
futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("10");
FieldUtils.writeField(BlockChain.getInstance(), "unitFees", Arrays.asList(originalFee, pastFeeIncrease, futureFeeIncrease), true);
assertEquals(originalFee.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(originalFee.timestamp));
assertEquals(pastFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(pastFeeIncrease.timestamp));
assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
// Send another payment transaction
// Fee should be determined automatically
transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 50000);
assertEquals(300000000L, transactionData.getFee().longValue());
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);
ValidationResult result = transaction.importAsUnconfirmed();
assertTrue("Transaction should be valid", ValidationResult.OK == result);
// Now try fetching and setting fee manually
transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 50000);
transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
assertEquals(300000000L, transactionData.getFee().longValue());
transaction = Transaction.fromData(repository, transactionData);
transaction.sign(alice);
result = transaction.importAsUnconfirmed();
assertTrue("Transaction should be valid", ValidationResult.OK == result);
}
}
// test reading the fee schedule from blockchain.json / test-chain-v2.json
@Test
public void testFeeScheduleInTestchainData() throws Exception {
try (final Repository repository = RepositoryManager.getRepository()) {
final long expectedFutureFeeIncreaseTimestamp = 9999999999999L; // 20 Nov 2286, as per test-chain-v2.json
final long expectedFutureFeeIncreaseValue = new AmountTypeAdapter().unmarshal("1");
assertEquals(expectedFutureFeeIncreaseValue, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp));
// Validate unit fees pre and post timestamp
assertEquals(10000000, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp - 1)); // 0.1 QORT
assertEquals(100000000, BlockChain.getInstance().getUnitFeeAtTimestamp(expectedFutureFeeIncreaseTimestamp)); // 1 QORT
// Payment
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
PaymentTransactionData transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), bob.getAddress(), 100000);
transactionData.setFee(new PaymentTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
assertEquals(10000000L, transactionData.getFee().longValue());
TransactionUtils.signAndMint(repository, transactionData, alice);
}
}
}

View File

@@ -51,89 +51,6 @@ public class OnlineAccountsTests extends Common {
}
@Test
public void testGetOnlineAccountsV2() throws MessageException {
List<OnlineAccountData> onlineAccountsOut = generateOnlineAccounts(false);
Message messageOut = new GetOnlineAccountsV2Message(onlineAccountsOut);
byte[] messageBytes = messageOut.toBytes();
ByteBuffer byteBuffer = ByteBuffer.wrap(messageBytes);
GetOnlineAccountsV2Message messageIn = (GetOnlineAccountsV2Message) Message.fromByteBuffer(byteBuffer);
List<OnlineAccountData> onlineAccountsIn = messageIn.getOnlineAccounts();
assertEquals("size mismatch", onlineAccountsOut.size(), onlineAccountsIn.size());
assertTrue("accounts mismatch", onlineAccountsIn.containsAll(onlineAccountsOut));
Message oldMessageOut = new GetOnlineAccountsMessage(onlineAccountsOut);
byte[] oldMessageBytes = oldMessageOut.toBytes();
long numTimestamps = onlineAccountsOut.stream().mapToLong(OnlineAccountData::getTimestamp).sorted().distinct().count();
System.out.println(String.format("For %d accounts split across %d timestamp%s: old size %d vs new size %d",
onlineAccountsOut.size(),
numTimestamps,
numTimestamps != 1 ? "s" : "",
oldMessageBytes.length,
messageBytes.length));
}
@Test
public void testOnlineAccountsV2() throws MessageException {
List<OnlineAccountData> onlineAccountsOut = generateOnlineAccounts(true);
Message messageOut = new OnlineAccountsV2Message(onlineAccountsOut);
byte[] messageBytes = messageOut.toBytes();
ByteBuffer byteBuffer = ByteBuffer.wrap(messageBytes);
OnlineAccountsV2Message messageIn = (OnlineAccountsV2Message) Message.fromByteBuffer(byteBuffer);
List<OnlineAccountData> onlineAccountsIn = messageIn.getOnlineAccounts();
assertEquals("size mismatch", onlineAccountsOut.size(), onlineAccountsIn.size());
assertTrue("accounts mismatch", onlineAccountsIn.containsAll(onlineAccountsOut));
Message oldMessageOut = new OnlineAccountsMessage(onlineAccountsOut);
byte[] oldMessageBytes = oldMessageOut.toBytes();
long numTimestamps = onlineAccountsOut.stream().mapToLong(OnlineAccountData::getTimestamp).sorted().distinct().count();
System.out.println(String.format("For %d accounts split across %d timestamp%s: old size %d vs new size %d",
onlineAccountsOut.size(),
numTimestamps,
numTimestamps != 1 ? "s" : "",
oldMessageBytes.length,
messageBytes.length));
}
private List<OnlineAccountData> generateOnlineAccounts(boolean withSignatures) {
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
int numTimestamps = RANDOM.nextInt(2) + 1; // 1 or 2
for (int t = 0; t < numTimestamps; ++t) {
int numAccounts = RANDOM.nextInt(3000);
for (int a = 0; a < numAccounts; ++a) {
byte[] sig = null;
if (withSignatures) {
sig = new byte[Transformer.SIGNATURE_LENGTH];
RANDOM.nextBytes(sig);
}
byte[] pubkey = new byte[Transformer.PUBLIC_KEY_LENGTH];
RANDOM.nextBytes(pubkey);
onlineAccounts.add(new OnlineAccountData(t << 32, sig, pubkey));
}
}
return onlineAccounts;
}
@Test
public void testOnlineAccountsModulusV1() throws IllegalAccessException, DataException {
try (final Repository repository = RepositoryManager.getRepository()) {

View File

@@ -26,41 +26,6 @@ public class OnlineAccountsV3Tests {
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
}
@Ignore("For informational use")
@Test
public void compareV2ToV3() throws MessageException {
List<OnlineAccountData> onlineAccounts = generateOnlineAccounts(false);
// How many of each timestamp and leading byte (of public key)
Map<Long, Map<Byte, byte[]>> hashesByTimestampThenByte = convertToHashMaps(onlineAccounts);
byte[] v3DataBytes = new GetOnlineAccountsV3Message(hashesByTimestampThenByte).toBytes();
int v3ByteSize = v3DataBytes.length;
byte[] v2DataBytes = new GetOnlineAccountsV2Message(onlineAccounts).toBytes();
int v2ByteSize = v2DataBytes.length;
int numTimestamps = hashesByTimestampThenByte.size();
System.out.printf("For %d accounts split across %d timestamp%s: V2 size %d vs V3 size %d%n",
onlineAccounts.size(),
numTimestamps,
numTimestamps != 1 ? "s" : "",
v2ByteSize,
v3ByteSize
);
for (var outerMapEntry : hashesByTimestampThenByte.entrySet()) {
long timestamp = outerMapEntry.getKey();
var innerMap = outerMapEntry.getValue();
System.out.printf("For timestamp %d: %d / 256 slots used.%n",
timestamp,
innerMap.size()
);
}
}
private Map<Long, Map<Byte, byte[]>> convertToHashMaps(List<OnlineAccountData> onlineAccounts) {
// How many of each timestamp and leading byte (of public key)
Map<Long, Map<Byte, byte[]>> hashesByTimestampThenByte = new HashMap<>();
@@ -200,7 +165,9 @@ public class OnlineAccountsV3Tests {
byte[] pubkey = new byte[Transformer.PUBLIC_KEY_LENGTH];
RANDOM.nextBytes(pubkey);
onlineAccounts.add(new OnlineAccountData(timestamp, sig, pubkey));
Integer nonce = RANDOM.nextInt();
onlineAccounts.add(new OnlineAccountData(timestamp, sig, pubkey, nonce));
}
}

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,8 +4,11 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 0,
"unitFee": "0.00000001",
"unitFees": [
{ "timestamp": 0, "fee": "0.00000001" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "0.00000001" },
{ "timestamp": 1645372800000, "fee": "5" }
],
"requireGroupForApproval": false,

View File

@@ -4,9 +4,13 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"unitFees": [
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 9999999999999, "fee": "1" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 1645372800000, "fee": "5" }
{ "timestamp": 0, "fee": "0.1" },
{ "timestamp": 9999999999999, "fee": "5" }
],
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,

View File

@@ -33,7 +33,8 @@ fi
# Limits Java JVM stack size and maximum heap usage.
# Comment out for bigger systems, e.g. non-routers
# or when API documentation is enabled
# JVM_MEMORY_ARGS="-Xss256k -Xmx128m"
# Uncomment (remove '#' sign) line below if your system has less than 12GB of RAM for optimal RAM defaults
# JVM_MEMORY_ARGS="-Xss1256k -Xmx3128m"
# Although java.net.preferIPv4Stack is supposed to be false
# by default in Java 11, on some platforms (e.g. FreeBSD 12),