Compare commits

...

58 Commits

Author SHA1 Message Date
CalDescent
3fbcc50503 Fixed deserialization issues with CreationRequest, added validation, and made small code tweaks for consistency with other endpoints. 2023-08-25 11:01:48 +01:00
b051f9be89 fix strings in request 2023-08-23 14:41:50 -05:00
24ff3ab581 base64 to byte array 2023-08-22 22:46:58 -05:00
34382b6e69 add endpoint 2023-08-22 19:58:24 -05:00
CalDescent
9428f9688f Merge pull request #126 from jschulthess/master
Fix website sub-folder rendering 404
2023-08-20 14:22:58 +01:00
CalDescent
9f4a0b7957 Merge pull request #127 from kennycud/master
consolidated shared functionality into BitcoinyTests.java
2023-08-20 14:22:18 +01:00
CalDescent
3c8574a466 Trade bot improvements
- Add async responder thread from @catbref which was previously only in place for LTC
- Log when computing mempow nonces
- Skip transaction import if signature is invalid
- Added checks to a message test to mimic trade bot transaction lookups
2023-08-20 12:42:49 +01:00
Jürg Schulthess
c428d7ce2e Merge branch 'Qortal:master' into master 2023-08-20 09:21:48 +02:00
CalDescent
4feb8f46c8 Updated testchain.json to use new unitFees structure. 2023-08-19 20:42:15 +01:00
CalDescent
1f7a60dfd8 Fixed long term issue preventing trade bot statuses from being logged correctly. 2023-08-19 20:27:05 +01:00
CalDescent
379b850bbd Increase default maxTransactionsPerBlock to 50 now that the process is faster. Can be increased much further in the future. 2023-08-19 14:12:12 +01:00
CalDescent
7bb61ec564 Update various transaction types at a future unknown timestamp.
- PUBLICIZE transactions are no longer possible.
- ARBITRARY transactions are now only possible using a fee.
- MESSAGE transactions only confirm when they are being sent to an AT. Messages to regular addresses (or no recipient) will expire after 24 hours.
- Difficulty for confirmed MESSAGE transactions increases from 14 to 16.
- Difficulty for unconfirmed MESSAGE transactions decreases from 14 to 12.
2023-08-19 13:57:26 +01:00
CalDescent
b0224651c2 Always use rate limiter for metadata requests, and sleep for a random amount of time between fetching metadata items. 2023-08-18 20:32:44 +01:00
CalDescent
6bf2b99913 Wait until unconfirmed transactions are considered to be valid before broadcasting them. 2023-08-18 15:37:24 +01:00
Jürg Schulthess
fd9d0c4e51 Merge branch 'Qortal:master' into master 2023-08-13 19:17:11 +02:00
kennycud
c2756a5872 Merge branch 'Qortal:master' into master 2023-08-13 05:32:47 -07:00
CalDescent
ecfb9a7d6d Bump version to 4.2.4 2023-08-12 19:34:59 +01:00
CalDescent
dd9d3fdb22 Add to the unconfirmed transactions cache when importing a transaction. 2023-08-12 19:27:19 +01:00
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
kennycud
fe840bbf02 consolidated shared functionality into BitcoinyTests.java 2023-08-08 12:17:29 -07:00
Jürg Schulthess
e7901a391f Merge branch 'Qortal:master' into master 2023-08-03 21:32:06 +02: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
Jürg Schulthess
18e880158f Merge branch 'Qortal:master' into master 2023-08-01 11:09:06 +02: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
Jürg Schulthess
a3526d84bc Merge branch 'Qortal:master' into master 2023-07-30 13:39:44 +02: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
Jürg Schulthess
ff7a87ab18 Merge branch 'Qortal:master' into master 2023-07-10 20:53:24 +02: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
Jürg Schulthess
cc8cdcd93a Fix website sub-folder rendering 404 2023-07-03 09:52:07 +02: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
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
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
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
90 changed files with 2403 additions and 1376 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.3</version>
<version>4.2.4</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,102 @@
package org.qortal.api.model;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlTransient;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.DecoderException;
@XmlAccessorType(XmlAccessType.FIELD)
public class AtCreationRequest {
@Schema(description = "CIYAM AT version", example = "2")
private short ciyamAtVersion;
@Schema(description = "base64-encoded code bytes", example = "")
private String codeBytesBase64;
@Schema(description = "base64-encoded data bytes", example = "")
private String dataBytesBase64;
private short numCallStackPages;
private short numUserStackPages;
private long minActivationAmount;
// Default constructor for JSON deserialization
public AtCreationRequest() {}
// Getters and setters
public short getCiyamAtVersion() {
return ciyamAtVersion;
}
public void setCiyamAtVersion(short ciyamAtVersion) {
this.ciyamAtVersion = ciyamAtVersion;
}
public String getCodeBytesBase64() {
return this.codeBytesBase64;
}
@XmlTransient
@Schema(hidden = true)
public byte[] getCodeBytes() {
if (this.codeBytesBase64 != null) {
try {
return Base64.decode(this.codeBytesBase64);
}
catch (DecoderException e) {
return null;
}
}
return null;
}
public String getDataBytesBase64() {
return this.dataBytesBase64;
}
@XmlTransient
@Schema(hidden = true)
public byte[] getDataBytes() {
if (this.dataBytesBase64 != null) {
try {
return Base64.decode(this.dataBytesBase64);
}
catch (DecoderException e) {
return null;
}
}
return null;
}
public short getNumCallStackPages() {
return numCallStackPages;
}
public void setNumCallStackPages(short numCallStackPages) {
this.numCallStackPages = numCallStackPages;
}
public short getNumUserStackPages() {
return numUserStackPages;
}
public void setNumUserStackPages(short numUserStackPages) {
this.numUserStackPages = numUserStackPages;
}
public long getMinActivationAmount() {
return minActivationAmount;
}
public void setMinActivationAmount(long minActivationAmount) {
this.minActivationAmount = minActivationAmount;
}
}

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

@@ -724,7 +724,7 @@ public class ArbitraryResource {
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
try {
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false);
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, true);
if (transactionMetadata != null) {
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, true);
if (resourceMetadata != null) {

View File

@@ -27,6 +27,7 @@ import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.api.model.AtCreationRequest;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -38,9 +39,14 @@ import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Base58;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Path("/at")
@Tag(name = "Automated Transactions")
public class AtResource {
private static final Logger logger = LoggerFactory.getLogger(AtResource.class);
@Context
HttpServletRequest request;
@@ -156,6 +162,52 @@ public class AtResource {
}
}
@POST
@Path("/create")
@Operation(
summary = "Create base58-encoded AT creation bytes from the provided parameters",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = AtCreationRequest.class
)
)
),
responses = {
@ApiResponse(
description = "AT creation bytes suitable for use in a DEPLOY_AT transaction",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
public String create(AtCreationRequest atCreationRequest) {
if (atCreationRequest.getCiyamAtVersion() < 2) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "ciyamAtVersion must be at least 2");
}
if (atCreationRequest.getCodeBytes() == null) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Valid codeBytesBase64 must be supplied");
}
if (atCreationRequest.getDataBytes() == null) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Valid dataBytesBase64 must be supplied");
}
byte[] creationBytes = MachineState.toCreationBytes(
atCreationRequest.getCiyamAtVersion(),
atCreationRequest.getCodeBytes(),
atCreationRequest.getDataBytes(),
atCreationRequest.getNumCallStackPages(),
atCreationRequest.getNumUserStackPages(),
atCreationRequest.getMinActivationAmount()
);
return Base58.encode(creationBytes);
}
@POST
@Operation(
summary = "Build raw, unsigned, DEPLOY_AT transaction",

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;
@@ -127,6 +127,11 @@ public class ArbitraryDataRenderer {
String filename = this.getFilename(unzippedPath, inPath);
Path filePath = Paths.get(unzippedPath, filename);
boolean usingCustomRouting = false;
if (Files.isDirectory(filePath) && (!inPath.endsWith("/"))) {
inPath = inPath + "/";
filename = this.getFilename(unzippedPath, inPath);
filePath = Paths.get(unzippedPath, filename);
}
// If the file doesn't exist, we may need to route the request elsewhere, or cleanup
if (!Files.exists(filePath)) {
@@ -159,7 +164,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

@@ -84,6 +84,7 @@ public class Block {
TRANSACTION_PROCESSING_FAILED(53),
TRANSACTION_ALREADY_PROCESSED(54),
TRANSACTION_NEEDS_APPROVAL(55),
TRANSACTION_NOT_CONFIRMABLE(56),
AT_STATES_MISMATCH(61),
ONLINE_ACCOUNTS_INVALID(70),
ONLINE_ACCOUNT_UNKNOWN(71),
@@ -130,6 +131,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 +567,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 +1054,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 +1145,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;
}
@@ -1234,6 +1252,13 @@ public class Block {
|| transaction.getDeadline() <= this.blockData.getTimestamp())
return ValidationResult.TRANSACTION_TIMESTAMP_INVALID;
// After feature trigger, check that this transaction is confirmable
if (transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
if (!transaction.isConfirmable()) {
return ValidationResult.TRANSACTION_NOT_CONFIRMABLE;
}
}
// Check transaction isn't already included in a block
if (this.repository.getTransactionRepository().isConfirmed(transactionData.getSignature()))
return ValidationResult.TRANSACTION_ALREADY_PROCESSED;

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) */
@@ -211,6 +209,9 @@ public class BlockChain {
/** Snapshot timestamp for self sponsorship algo V1 */
private long selfSponsorshipAlgoV1SnapshotTimestamp;
/** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */
private long mempowTransactionUpdatesTimestamp;
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@@ -346,10 +347,6 @@ public class BlockChain {
return this.isTestChain;
}
public long getUnitFee() {
return this.unitFee;
}
public int getMaxBytesPerUnitFee() {
return this.maxBytesPerUnitFee;
}
@@ -376,6 +373,11 @@ public class BlockChain {
return this.selfSponsorshipAlgoV1SnapshotTimestamp;
}
// Feature-trigger timestamp to modify behaviour of various transactions that support mempow
public long getMemPoWTransactionUpdatesTimestamp() {
return this.mempowTransactionUpdatesTimestamp;
}
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
public boolean getRequireGroupForApproval() {
return this.requireGroupForApproval;
@@ -547,13 +549,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) {
@@ -215,12 +218,6 @@ public class TransactionImporter extends Thread {
LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size());
}
if (!newlyValidSignatures.isEmpty()) {
LOGGER.debug("Broadcasting {} newly valid signatures ahead of import", newlyValidSignatures.size());
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyValidSignatures);
Network.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
}
} catch (DataException e) {
LOGGER.error("Repository issue while processing incoming transactions", e);
}
@@ -254,6 +251,15 @@ 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;
// A list of signatures were imported in this round
List<byte[]> newlyImportedSignatures = new ArrayList<>();
// Import transactions with valid signatures
try {
for (int i = 0; i < sigValidTransactions.size(); ++i) {
@@ -286,6 +292,15 @@ public class TransactionImporter extends Thread {
case OK: {
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
// Add to the unconfirmed transactions cache
if (transactionData.getType() != Transaction.TransactionType.CHAT && unconfirmedTransactionsCache != null) {
unconfirmedTransactionsCache.add(transactionData);
}
// Signature imported in this round
newlyImportedSignatures.add(transactionData.getSignature());
break;
}
@@ -314,9 +329,18 @@ public class TransactionImporter extends Thread {
// Transaction has been processed, even if only to reject it
removeIncomingTransaction(transactionData.getSignature());
}
if (!newlyImportedSignatures.isEmpty()) {
LOGGER.debug("Broadcasting {} newly imported signatures", newlyImportedSignatures.size());
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyImportedSignatures);
Network.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
}
} 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

@@ -275,7 +275,10 @@ public class ArbitraryDataManager extends Thread {
int offset = 0;
while (!isStopping) {
Thread.sleep(1000L);
final int minSeconds = 3;
final int maxSeconds = 10;
final int randomSleepTime = new Random().nextInt((maxSeconds - minSeconds + 1)) + minSeconds;
Thread.sleep(randomSleepTime * 1000L);
// Any arbitrary transactions we want to fetch data for?
try (final Repository repository = RepositoryManager.getRepository()) {

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

@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,36 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@@ -548,15 +565,25 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
@@ -668,15 +695,25 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}

View File

@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,36 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@@ -548,15 +565,25 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
@@ -668,15 +695,25 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}

View File

@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,36 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@@ -548,15 +565,25 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
@@ -668,15 +695,25 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}

View File

@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,36 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@@ -548,15 +565,25 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
@@ -668,15 +695,25 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}

View File

@@ -325,15 +325,24 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
@@ -556,15 +565,25 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
@@ -676,15 +695,25 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}

View File

@@ -20,6 +20,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -320,20 +321,36 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddressT3));
@@ -561,15 +578,25 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
@@ -681,15 +708,25 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}

View File

@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,36 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
@@ -548,15 +565,25 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", outgoingMessageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
outgoingMessageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) outgoingMessageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), outgoingMessageTransaction.getPoWDifficulty());
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (outgoingMessageTransaction.isSignatureValid()) {
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}
@@ -668,15 +695,25 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient);
messageTransaction.computeNonce();
MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData();
LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty());
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
if (messageTransaction.isSignatureValid()) {
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
else {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: signature invalid", messageRecipient));
return;
}
}

View File

@@ -333,7 +333,7 @@ public class TradeBot implements Listener {
SysTray.getInstance().showMessage("Trade-Bot", String.format("%s: %s", tradeBotData.getAtAddress(), newState), MessageType.INFO);
if (logMessageSupplier != null)
LOGGER.info(logMessageSupplier);
LOGGER.info(logMessageSupplier.get());
LOGGER.debug(() -> String.format("new state for trade-bot entry based on AT %s: %s", tradeBotData.getAtAddress(), newState));

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

@@ -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 = 50;
/** 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,7 +227,7 @@ 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.2";
@@ -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

@@ -1,6 +1,5 @@
package org.qortal.transaction;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -8,7 +7,6 @@ import java.util.stream.Collectors;
import org.qortal.account.Account;
import org.qortal.block.BlockChain;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.MemoryPoW;
@@ -88,8 +86,14 @@ public class ArbitraryTransaction extends Transaction {
if (this.transactionData.getFee() < 0)
return ValidationResult.NEGATIVE_FEE;
// After the feature trigger, we require the fee to be sufficient if it's not 0.
// If the fee is zero, then the nonce is validated in isSignatureValid() as an alternative to a fee
// As of the mempow transaction updates timestamp, a nonce is no longer supported, so a valid fee must be included
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
// Validate the fee
return super.isFeeValid();
}
// After the earlier "optional fee" feature trigger, we required the fee to be sufficient if it wasn't 0.
// If the fee was zero, then the nonce was validated in isSignatureValid() as an alternative to a fee
if (this.arbitraryTransactionData.getTimestamp() >= BlockChain.getInstance().getArbitraryOptionalFeeTimestamp() && this.arbitraryTransactionData.getFee() != 0L) {
return super.isFeeValid();
}
@@ -214,7 +218,13 @@ public class ArbitraryTransaction extends Transaction {
// Clear nonce from transactionBytes
ArbitraryTransactionTransformer.clearNonce(transactionBytes);
// As of feature-trigger timestamp, we only require a nonce when the fee is zero
// As of the mempow transaction updates timestamp, a nonce is no longer supported, so a fee must be included
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
// Require that the fee is a positive number. Fee checking itself is performed in isFeeValid()
return (this.arbitraryTransactionData.getFee() > 0L);
}
// As of the earlier "optional fee" feature-trigger timestamp, we only required a nonce when the fee was zero
boolean beforeFeatureTrigger = this.arbitraryTransactionData.getTimestamp() < BlockChain.getInstance().getArbitraryOptionalFeeTimestamp();
if (beforeFeatureTrigger || this.arbitraryTransactionData.getFee() == 0L) {
// We only need to check nonce for recent transactions due to PoW verification overhead

View File

@@ -148,6 +148,12 @@ public class ChatTransaction extends Transaction {
// Nothing to do
}
@Override
public boolean isConfirmable() {
// CHAT transactions can't go into blocks
return false;
}
@Override
public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import

View File

@@ -33,7 +33,9 @@ public class MessageTransaction extends Transaction {
public static final int MAX_DATA_SIZE = 4000;
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
public static final int POW_DIFFICULTY = 14; // leading zero bits
public static final int POW_DIFFICULTY_V1 = 14; // leading zero bits
public static final int POW_DIFFICULTY_V2_CONFIRMABLE = 16; // leading zero bits
public static final int POW_DIFFICULTY_V2_UNCONFIRMABLE = 12; // leading zero bits
// Properties
@@ -109,7 +111,17 @@ public class MessageTransaction extends Transaction {
MessageTransactionTransformer.clearNonce(transactionBytes);
// Calculate nonce
this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY));
this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty()));
}
public int getPoWDifficulty() {
// The difficulty changes at the "mempow transactions updates" timestamp
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
// If this message is confirmable then require a higher difficulty
return this.isConfirmable() ? POW_DIFFICULTY_V2_CONFIRMABLE : POW_DIFFICULTY_V2_UNCONFIRMABLE;
}
// Before feature trigger timestamp, so use existing difficulty value
return POW_DIFFICULTY_V1;
}
/**
@@ -183,6 +195,18 @@ public class MessageTransaction extends Transaction {
return super.hasValidReference();
}
@Override
public boolean isConfirmable() {
// After feature trigger timestamp, only messages to an AT address can confirm
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
if (this.messageTransactionData.getRecipient() == null || !this.messageTransactionData.getRecipient().toUpperCase().startsWith("A")) {
// Message isn't to an AT address, so this transaction is unconfirmable
return false;
}
}
return true;
}
@Override
public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import
@@ -235,7 +259,7 @@ public class MessageTransaction extends Transaction {
MessageTransactionTransformer.clearNonce(transactionBytes);
// Check nonce
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, getPoWDifficulty(), nonce);
}
@Override
@@ -256,6 +280,11 @@ public class MessageTransaction extends Transaction {
@Override
public void process() throws DataException {
// Only certain MESSAGE transactions are able to confirm
if (!this.isConfirmable()) {
throw new DataException("Unconfirmable MESSAGE transactions should never be processed");
}
// If we have no amount then there's nothing to do
if (this.messageTransactionData.getAmount() == 0L)
return;
@@ -280,6 +309,11 @@ public class MessageTransaction extends Transaction {
@Override
public void orphan() throws DataException {
// Only certain MESSAGE transactions are able to confirm
if (!this.isConfirmable()) {
throw new DataException("Unconfirmable MESSAGE transactions should never be orphaned");
}
// If we have no amount then there's nothing to do
if (this.messageTransactionData.getAmount() == 0L)
return;

View File

@@ -155,6 +155,12 @@ public class PresenceTransaction extends Transaction {
// Nothing to do
}
@Override
public boolean isConfirmable() {
// PRESENCE transactions can't go into blocks
return false;
}
@Override
public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import

View File

@@ -7,6 +7,7 @@ import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.crypto.MemoryPoW;
import org.qortal.data.transaction.PublicizeTransactionData;
import org.qortal.data.transaction.TransactionData;
@@ -89,6 +90,12 @@ public class PublicizeTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Disable completely after feature-trigger timestamp, at the same time that mempow difficulties are being increased.
// It could be enabled again in the future, but preferably with an enforced minimum fee instead of allowing a mempow nonce.
if (this.transactionData.getTimestamp() >= BlockChain.getInstance().getMemPoWTransactionUpdatesTimestamp()) {
return ValidationResult.NOT_SUPPORTED;
}
// There can be only one
List<byte[]> signatures = this.repository.getTransactionRepository().getSignaturesMatchingCriteria(
TransactionType.PUBLICIZE,

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;
@@ -247,7 +248,8 @@ public abstract class Transaction {
GROUP_APPROVAL_REQUIRED(98),
ACCOUNT_NOT_TRANSFERABLE(99),
INVALID_BUT_OK(999),
NOT_YET_RELEASED(1000);
NOT_YET_RELEASED(1000),
NOT_SUPPORTED(1001);
public final int value;
@@ -377,7 +379,7 @@ public abstract class Transaction {
* @return
*/
public long getUnitFee(Long timestamp) {
return BlockChain.getInstance().getUnitFee();
return BlockChain.getInstance().getUnitFeeAtTimestamp(timestamp);
}
/**
@@ -617,7 +619,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
@@ -632,7 +637,7 @@ public abstract class Transaction {
}
/**
* Returns sorted, unconfirmed transactions, excluding invalid.
* Returns sorted, unconfirmed transactions, excluding invalid and unconfirmable.
*
* @return sorted, unconfirmed transactions
* @throws DataException
@@ -641,7 +646,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());
@@ -650,7 +655,8 @@ public abstract class Transaction {
TransactionData transactionData = unconfirmedTransactionsIterator.next();
Transaction transaction = Transaction.fromData(repository, transactionData);
if (transaction.isStillValidUnconfirmed(latestBlockData.getTimestamp()) != ValidationResult.OK)
// Must be confirmable and valid
if (!transaction.isConfirmable() || transaction.isStillValidUnconfirmed(latestBlockData.getTimestamp()) != ValidationResult.OK)
unconfirmedTransactionsIterator.remove();
}
@@ -888,6 +894,17 @@ public abstract class Transaction {
/* To be optionally overridden */
}
/**
* Returns whether transaction is 'confirmable' - i.e. is of a type that
* can be included in a block. Some transactions are 'unconfirmable'
* and therefore must remain in the mempool until they expire.
* @return
*/
public boolean isConfirmable() {
/* To be optionally overridden */
return true;
}
/**
* Returns whether transaction can be added to the blockchain.
* <p>

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" }
],
@@ -25,6 +29,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 1659801600000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
"mempowTransactionUpdatesTimestamp": 9999999999999,
"rewardsByHeight": [
{ "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 },

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

@@ -3,6 +3,8 @@ package org.qortal.test;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crypto.MemoryPoW;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import static org.junit.Assert.*;
@@ -39,13 +41,14 @@ public class MemoryPoWTests {
}
@Test
public void testMultipleComputes() {
public void testMultipleComputes() throws DataException {
Common.useDefaultSettings();
Random random = new Random();
final int sampleSize = 20;
final int sampleSize = 10;
final long stddevDivisor = sampleSize * (sampleSize - 1);
for (int difficulty = 8; difficulty < 16; difficulty += 2) {
for (int difficulty = 8; difficulty <= 16; difficulty++) {
byte[] data = new byte[256];
long[] times = new long[sampleSize];

View File

@@ -1,5 +1,6 @@
package org.qortal.test;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -14,12 +15,9 @@ import org.qortal.group.Group.ApprovalThreshold;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.GroupUtils;
import org.qortal.test.common.TestAccount;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.*;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
@@ -31,6 +29,7 @@ import org.qortal.utils.NTP;
import static org.junit.Assert.*;
import java.util.List;
import java.util.Random;
public class MessageTests extends Common {
@@ -85,7 +84,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");
@@ -139,7 +138,7 @@ public class MessageTests extends Common {
}
@Test
public void withRecipentWithAmount() throws DataException {
public void withRecipientWithAmount() throws DataException {
testMessage(Group.NO_GROUP, recipient, 123L, Asset.QORT);
}
@@ -153,6 +152,140 @@ public class MessageTests extends Common {
testMessage(1, null, 0L, null);
}
@Test
public void atRecipientNoFeeWithNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String atRecipient = deployAt();
MessageTransaction transaction = testFeeNonce(repository, false, true, atRecipient, true);
// Transaction should be confirmable because it's to an AT, and therefore should be present in a block
assertTrue(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertTrue(isTransactionConfirmed(repository, transaction));
assertEquals(16, transaction.getPoWDifficulty());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void regularRecipientNoFeeWithNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
// Transaction should not be present in db yet
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, recipient, null, null, null);
assertTrue(messageTransactionsData.isEmpty());
MessageTransaction transaction = testFeeNonce(repository, false, true, recipient, true);
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
assertFalse(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertFalse(isTransactionConfirmed(repository, transaction));
assertEquals(12, transaction.getPoWDifficulty());
// Transaction should be found when trade bot searches for it
messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, recipient, null, null, null);
assertEquals(1, messageTransactionsData.size());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void noRecipientNoFeeWithNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
MessageTransaction transaction = testFeeNonce(repository, false, true, null, true);
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
assertFalse(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertFalse(isTransactionConfirmed(repository, transaction));
assertEquals(12, transaction.getPoWDifficulty());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void atRecipientWithFeeNoNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String atRecipient = deployAt();
MessageTransaction transaction = testFeeNonce(repository, true, false, atRecipient, true);
// Transaction should be confirmable because it's to an AT, and therefore should be present in a block
assertTrue(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertTrue(isTransactionConfirmed(repository, transaction));
assertEquals(16, transaction.getPoWDifficulty());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void regularRecipientWithFeeNoNonce() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
MessageTransaction transaction = testFeeNonce(repository, true, false, recipient, true);
// Transaction shouldn't be confirmable because it's not to an AT, and therefore shouldn't be present in a block
assertFalse(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertFalse(isTransactionConfirmed(repository, transaction));
assertEquals(12, transaction.getPoWDifficulty());
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void atRecipientNoFeeWithNonceLegacyDifficulty() throws DataException, IllegalAccessException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Set mempowTransactionUpdatesTimestamp to a high value, so that it hasn't activated key
FieldUtils.writeField(BlockChain.getInstance(), "mempowTransactionUpdatesTimestamp", Long.MAX_VALUE, true);
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String atRecipient = deployAt();
MessageTransaction transaction = testFeeNonce(repository, false, true, atRecipient, true);
// Transaction should be confirmable because all MESSAGE transactions confirmed prior to the feature trigger
assertTrue(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertTrue(isTransactionConfirmed(repository, transaction));
assertEquals(14, transaction.getPoWDifficulty()); // Legacy difficulty was 14 in all cases
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void regularRecipientNoFeeWithNonceLegacyDifficulty() throws DataException, IllegalAccessException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Set mempowTransactionUpdatesTimestamp to a high value, so that it hasn't activated key
FieldUtils.writeField(BlockChain.getInstance(), "mempowTransactionUpdatesTimestamp", Long.MAX_VALUE, true);
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
MessageTransaction transaction = testFeeNonce(repository, false, true, recipient, true);
// Transaction should be confirmable because all MESSAGE transactions confirmed prior to the feature trigger
assertTrue(transaction.isConfirmable());
TransactionUtils.signAndMint(repository, transaction.getTransactionData(), alice);
assertTrue(isTransactionConfirmed(repository, transaction)); // All MESSAGE transactions would confirm before feature trigger
assertEquals(14, transaction.getPoWDifficulty()); // Legacy difficulty was 14 in all cases
BlockUtils.orphanLastBlock(repository);
}
}
@Test
public void serializationTests() throws DataException, TransformationException {
// with recipient, with amount
@@ -165,6 +298,24 @@ public class MessageTests extends Common {
testSerialization(null, 0L, null);
}
private String deployAt() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
byte[] creationBytes = AtUtils.buildSimpleAT();
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = AtUtils.doDeployAT(repository, deployer, creationBytes, fundingAmount);
String address = deployAtTransaction.getATAccount().getAddress();
assertNotNull(address);
return address;
}
}
private boolean isTransactionConfirmed(Repository repository, MessageTransaction transaction) throws DataException {
TransactionData queriedTransactionData = repository.getTransactionRepository().fromSignature(transaction.getTransactionData().getSignature());
return queriedTransactionData.getBlockHeight() != null && queriedTransactionData.getBlockHeight() > 0;
}
private boolean isValid(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
@@ -195,41 +346,48 @@ public class MessageTests extends Common {
return messageTransaction.hasValidReference();
}
private void testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException {
private MessageTransaction testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
int txGroupId = 0;
int nonce = 0;
long amount = 0;
long assetId = Asset.QORT;
byte[] data = new byte[1];
boolean isText = false;
boolean isEncrypted = false;
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
MessageTransaction transaction = new MessageTransaction(repository, transactionData);
if (withFee)
transactionData.setFee(transaction.calcRecommendedFee());
else
transactionData.setFee(0L);
if (withNonce) {
transaction.computeNonce();
} else {
transactionData.setNonce(-1);
}
transaction.sign(alice);
assertEquals(isValid, transaction.isSignatureValid());
return testFeeNonce(repository, withFee, withNonce, recipient, isValid);
}
}
private void testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
private MessageTransaction testFeeNonce(Repository repository, boolean withFee, boolean withNonce, String recipient, boolean isValid) throws DataException {
TestAccount alice = Common.getTestAccount(repository, "alice");
int txGroupId = 0;
int nonce = 0;
long amount = 0;
long assetId = Asset.QORT;
byte[] data = new byte[1];
boolean isText = false;
boolean isEncrypted = false;
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
MessageTransaction transaction = new MessageTransaction(repository, transactionData);
if (withFee)
transactionData.setFee(transaction.calcRecommendedFee());
else
transactionData.setFee(0L);
if (withNonce) {
transaction.computeNonce();
} else {
transactionData.setNonce(-1);
}
transaction.sign(alice);
assertEquals(isValid, transaction.isSignatureValid());
return transaction;
}
private MessageTransaction testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
@@ -244,6 +402,8 @@ public class MessageTests extends Common {
TransactionUtils.signAndMint(repository, transactionData, alice);
BlockUtils.orphanLastBlock(repository);
return new MessageTransaction(repository, transactionData);
}
}

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

@@ -11,6 +11,7 @@ import org.qortal.arbitrary.ArbitraryDataReader;
import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
import org.qortal.data.transaction.ArbitraryTransactionData;
@@ -24,6 +25,7 @@ import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.io.IOException;
import java.nio.file.Files;
@@ -106,8 +108,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category);
// Check the chunk count is correct
@@ -157,8 +160,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category);
// Check the chunk count is correct
@@ -220,8 +224,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category);
// Check the chunk count is correct
@@ -272,8 +277,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category);
// Check the chunk count is correct
@@ -316,8 +322,9 @@ public class ArbitraryTransactionMetadataTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category);
// Check the metadata is correct

View File

@@ -10,6 +10,7 @@ import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
@@ -50,51 +51,6 @@ public class ArbitraryTransactionTests extends Common {
Common.useDefaultSettings();
}
@Test
public void testDifficultyTooLow() throws IllegalAccessException, DataException, IOException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String publicKey58 = Base58.encode(alice.getPublicKey());
String name = "TEST"; // Can be anything for this test
String identifier = null; // Not used for this test
Service service = Service.ARBITRARY_DATA;
int chunkSize = 100;
int dataLength = 900; // Actual data length will be longer due to encryption
// Register the name to Alice
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
// Set difficulty to 1
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
// Check that nonce validation succeeds
byte[] signature = arbitraryDataFile.getSignature();
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
ArbitraryTransaction transaction = new ArbitraryTransaction(repository, transactionData);
assertTrue(transaction.isSignatureValid());
// Increase difficulty to 15
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 15, true);
// Make sure the nonce validation fails
// Note: there is a very tiny chance this could succeed due to being extremely lucky
// and finding a high difficulty nonce in the first couple of cycles. It will be rare
// enough that we shouldn't need to account for it.
assertFalse(transaction.isSignatureValid());
// Reduce difficulty back to 1, to double check
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
assertTrue(transaction.isSignatureValid());
}
}
@Test
public void testNonceAndFee() throws IllegalAccessException, DataException, IOException {
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -497,8 +453,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
null, null, null, null);
byte[] signature = arbitraryDataFile.getSignature();
@@ -556,8 +513,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
title, description, tags, category);
byte[] signature = arbitraryDataFile.getSignature();
@@ -614,8 +572,9 @@ public class ArbitraryTransactionTests extends Common {
// Create PUT transaction
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength, true);
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true,
identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, fee, false,
null, null, null, null);
byte[] signature = arbitraryDataFile.getSignature();

View File

@@ -5,10 +5,12 @@ import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service;
import org.qortal.block.BlockChain;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction;
import org.qortal.utils.NTP;
import java.io.BufferedWriter;
import java.io.File;
@@ -20,16 +22,15 @@ import java.nio.file.Paths;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.assertEquals;
public class ArbitraryUtils {
public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier,
ArbitraryTransactionData.Method method, Service service, PrivateKeyAccount account,
int chunkSize) throws DataException {
long fee = BlockChain.getInstance().getUnitFeeAtTimestamp(NTP.getTime());
return ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, method, service,
account, chunkSize, 0L, true, null, null, null, null);
account, chunkSize, fee, false, null, null, null, null);
}
public static ArbitraryDataFile createAndMintTxn(Repository repository, String publicKey58, Path path, String name, String identifier,
@@ -47,7 +48,9 @@ public class ArbitraryUtils {
}
ArbitraryTransactionData transactionData = txnBuilder.getArbitraryTransactionData();
Transaction.ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, account);
assertEquals(Transaction.ValidationResult.OK, result);
if (result != Transaction.ValidationResult.OK) {
throw new DataException(String.format("Arbitrary transaction invalid: %s", result.toString()));
}
BlockUtils.mintBlock(repository);
// We need a new ArbitraryDataFile instance because the files will have been moved to the signature's folder

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

@@ -1,121 +1,59 @@
package org.qortal.test.crosschain;
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;
import org.junit.Test;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import org.qortal.crosschain.Bitcoiny;
public class BitcoinTests extends Common {
public class BitcoinTests extends BitcoinyTests {
private Bitcoin bitcoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
bitcoin = Bitcoin.getInstance();
@Override
protected String getCoinName() {
return "Bitcoin";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "BTC";
}
@Override
protected Bitcoiny getCoin() {
return Bitcoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Bitcoin.resetForTesting();
bitcoin = null;
}
@Override
protected String getDeterministicKey58() {
return "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
}
@Override
protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
}
@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"));
long before = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
public void testGetMedianBlockTime() {}
@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";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = bitcoin.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
// Check spent key caching doesn't affect outcome
transaction = bitcoin.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
}
public void testBuildSpend() {}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = bitcoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(bitcoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = bitcoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(bitcoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
public void testGetWalletBalance() {}
@Test
@Ignore("Often fails due to unreliable BTC testnet ElectrumX servers")
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = bitcoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
public void testGetUnusedReceiveAddress() {}
}

View File

@@ -0,0 +1,130 @@
package org.qortal.test.crosschain;
import org.bitcoinj.core.Transaction;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import java.util.Arrays;
import static org.junit.Assert.*;
public abstract class BitcoinyTests extends Common {
protected Bitcoiny bitcoiny;
protected abstract String getCoinName();
protected abstract String getCoinSymbol();
protected abstract Bitcoiny getCoin();
protected abstract void resetCoinForTesting();
protected abstract String getDeterministicKey58();
protected abstract String getRecipient();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
bitcoiny = getCoin();
}
@After
public void afterTest() {
resetCoinForTesting();
bitcoiny = null;
}
@Test
public void testGetMedianBlockTime() throws ForeignBlockchainException {
System.out.println(String.format("Starting " + getCoinSymbol() + " instance..."));
System.out.println(String.format(getCoinSymbol() + " instance started"));
long before = System.currentTimeMillis();
System.out.println(String.format(getCoinName() + " median blocktime: %d", bitcoiny.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format(getCoinName() + " median blocktime: %d", bitcoiny.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
makeGetMedianBlockTimeAssertions(firstPeriod, secondPeriod);
}
public void makeGetMedianBlockTimeAssertions(long firstPeriod, long secondPeriod) {
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoiny, p2shAddress);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testBuildSpend() {
String xprv58 = getDeterministicKey58();
String recipient = getRecipient();
long amount = 1000L;
Transaction transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
// Check spent key caching doesn't affect outcome
transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = getDeterministicKey58();
Long balance = bitcoiny.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(bitcoiny.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = bitcoiny.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(bitcoiny.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = getDeterministicKey58();
String address = bitcoiny.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@@ -1,115 +1,48 @@
package org.qortal.test.crosschain;
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;
import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Digibyte;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class DigibyteTests extends Common {
public class DigibyteTests extends BitcoinyTests {
private Digibyte digibyte;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
digibyte = Digibyte.getInstance();
@Override
protected String getCoinName() {
return "Digibyte";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "DGB";
}
@Override
protected Bitcoiny getCoin() {
return Digibyte.getInstance();
}
@Override
protected void resetCoinForTesting() {
Digibyte.resetForTesting();
digibyte = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
}
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
@Override
protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(digibyte, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
public void testBuildSpend() {}
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = digibyte.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = digibyte.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
Long balance = digibyte.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(digibyte.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = digibyte.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(digibyte.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
String address = digibyte.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@@ -1,115 +1,47 @@
package org.qortal.test.crosschain;
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.BitcoinyHTLC;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Dogecoin;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import java.util.Arrays;
public class DogecoinTests extends BitcoinyTests {
import static org.junit.Assert.*;
public class DogecoinTests extends Common {
private Dogecoin dogecoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
dogecoin = Dogecoin.getInstance();
@Override
protected String getCoinName() {
return "Dogecoin";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "DOGE";
}
@Override
protected Bitcoiny getCoin() {
return Dogecoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Dogecoin.resetForTesting();
dogecoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
}
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
@Override
protected String getRecipient() {
return null;
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(dogecoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() {
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
String recipient = "DP1iFao33xdEPa5vaArpj7sykfzKNeiJeX";
long amount = 1000L;
Transaction transaction = dogecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = dogecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
Long balance = dogecoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(dogecoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = dogecoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(dogecoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
String address = dogecoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
public void testBuildSpend() {}
}

View File

@@ -1,113 +1,43 @@
package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class LitecoinTests extends Common {
public class LitecoinTests extends BitcoinyTests {
private Litecoin litecoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
litecoin = Litecoin.getInstance();
@Override
protected String getCoinName() {
return "Litecoin";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "LTC";
}
@Override
protected Bitcoiny getCoin() {
return Litecoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Litecoin.resetForTesting();
litecoin = null;
}
@Test
public void testGetMedianBlockTime() throws ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
}
System.out.println(String.format("Litecoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
@Override
protected String getRecipient() {
return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(litecoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = litecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = litecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = litecoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(litecoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = litecoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(litecoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = litecoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
public void testFindHtlcSecret() {}
}

View File

@@ -3,57 +3,53 @@ package org.qortal.test.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
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.controller.tradebot.TradeBot;
import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import org.qortal.transform.TransformationException;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
import static org.qortal.crosschain.BitcoinyHTLC.Status.*;
public class PirateChainTests extends Common {
public class PirateChainTests extends BitcoinyTests {
private PirateChain pirateChain;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
pirateChain = PirateChain.getInstance();
@Override
protected String getCoinName() {
return "PirateChain";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "ARRR";
}
@Override
protected Bitcoiny getCoin() {
return PirateChain.getInstance();
}
@Override
protected void resetCoinForTesting() {
Litecoin.resetForTesting();
pirateChain = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return null;
}
System.out.println(String.format("Pirate Chain median blocktime: %d", pirateChain.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
@Override
protected String getRecipient() {
return null;
}
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("1st call should take less than 5 seconds", firstPeriod < 5000L);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
public void makeGetMedianBlockTimeAssertions(long firstPeriod, long secondPeriod) {
assertTrue("1st call should take less than 5 seconds", firstPeriod < 5000L);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
@@ -62,7 +58,7 @@ public class PirateChainTests extends Common {
int count = 20;
long before = System.currentTimeMillis();
List<CompactBlock> compactBlocks = pirateChain.getCompactBlocks(startHeight, count);
List<CompactBlock> compactBlocks = ((PirateChain) bitcoiny).getCompactBlocks(startHeight, count);
long after = System.currentTimeMillis();
System.out.println(String.format("Retrieval took: %d ms", after-before));
@@ -82,7 +78,7 @@ public class PirateChainTests extends Common {
Bytes.reverse(txBytes);
String txHashBE = HashCode.fromBytes(txBytes).toString();
byte[] rawTransaction = pirateChain.getBlockchainProvider().getRawTransaction(txHashBE);
byte[] rawTransaction = bitcoiny.getBlockchainProvider().getRawTransaction(txHashBE);
assertNotNull(rawTransaction);
}
@@ -121,7 +117,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
long p2shFee = 10000;
final long minimumAmount = 10000 + p2shFee;
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount);
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
assertEquals(FUNDED, htlcStatus);
}
@@ -130,7 +126,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "bYZrzSSgGp8aEGvihukoMGU8sXYrx19Wka";
long p2shFee = 10000;
final long minimumAmount = 10000 + p2shFee;
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount);
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
assertEquals(REDEEMED, htlcStatus);
}
@@ -139,14 +135,14 @@ public class PirateChainTests extends Common {
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv";
long p2shFee = 10000;
final long minimumAmount = 10000 + p2shFee;
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount);
BitcoinyHTLC.Status htlcStatus = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
assertEquals(REFUNDED, htlcStatus);
}
@Test
public void testGetTxidForUnspentAddress() throws ForeignBlockchainException {
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
String txid = PirateChainHTLC.getFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress);
String txid = PirateChainHTLC.getFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress);
// Reverse the byte order of the txid used by block explorers, to get to big-endian form
byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes();
@@ -161,7 +157,7 @@ public class PirateChainTests extends Common {
String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";
long p2shFee = 10000;
final long minimumAmount = 10000 + p2shFee;
String txid = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmount);
String txid = PirateChainHTLC.getUnspentFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress, minimumAmount);
// Reverse the byte order of the txid used by block explorers, to get to big-endian form
byte[] expectedTxidLE = HashCode.fromString("fea4b0c1abcf8f0f3ddc2fa2f9438501ee102aad62a9ff18a5ce7d08774755c0").asBytes();
@@ -174,7 +170,7 @@ public class PirateChainTests extends Common {
@Test
public void testGetTxidForSpentAddress() throws ForeignBlockchainException {
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA";
String txid = PirateChainHTLC.getFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress);
String txid = PirateChainHTLC.getFundingTxid(bitcoiny.getBlockchainProvider(), p2shAddress);
// Reverse the byte order of the txid used by block explorers, to get to big-endian form
byte[] expectedTxidLE = HashCode.fromString("fb386fc8eea0fbf3ea37047726b92c39441652b32d8d62a274331687f7a1eca8").asBytes();
@@ -187,7 +183,7 @@ public class PirateChainTests extends Common {
@Test
public void testGetTransactionsForAddress() throws ForeignBlockchainException {
String p2shAddress = "bE49izfVxz8odhu8c2BcUaVFUnt7NLFRgv"; //"t3KtVxeEb8srJofo6atMEpMpEP6TjEi8VqA";
List<BitcoinyTransaction> transactions = pirateChain.getBlockchainProvider()
List<BitcoinyTransaction> transactions = bitcoiny.getBlockchainProvider()
.getAddressBitcoinyTransactions(p2shAddress, false);
assertEquals(2, transactions.size());
@@ -232,66 +228,17 @@ public class PirateChainTests extends Common {
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(pirateChain, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = pirateChain.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = pirateChain.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
public void testBuildSpend() {}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = pirateChain.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(pirateChain.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = pirateChain.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(pirateChain.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
public void testGetWalletBalance() {}
@Test
@Ignore(value = "Needs adapting for Pirate Chain")
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = pirateChain.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}
public void testGetUnusedReceiveAddress() {}
}

View File

@@ -1,115 +1,47 @@
package org.qortal.test.crosschain;
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;
import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Ravencoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class RavencoinTests extends Common {
public class RavencoinTests extends BitcoinyTests {
private Ravencoin ravencoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
ravencoin = Ravencoin.getInstance();
@Override
protected String getCoinName() {
return "Ravencoin";
}
@After
public void afterTest() {
@Override
protected String getCoinSymbol() {
return "RVN";
}
@Override
protected Bitcoiny getCoin() {
return Ravencoin.getInstance();
}
@Override
protected void resetCoinForTesting() {
Ravencoin.resetForTesting();
ravencoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
@Override
protected String getDeterministicKey58() {
return "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
}
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
@Override
protected String getRecipient() {
return null;
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(ravencoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
public void testFindHtlcSecret() {}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = ravencoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = ravencoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
Long balance = ravencoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(ravencoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = ravencoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(ravencoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
String address = ravencoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
public void testBuildSpend() {}
}

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,124 @@ 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");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
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();
assertEquals("Transaction should be valid", ValidationResult.OK, result);
// Now try fetching and setting fee manually
transactionData = new PaymentTransactionData(TestTransaction.generateBase(alice), chloe.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();
assertEquals("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,
@@ -15,6 +18,7 @@
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -19,6 +22,7 @@
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 9999999999999,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -19,6 +22,7 @@
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +23,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

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,
@@ -20,6 +24,7 @@
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 9999999999999,
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
"mempowTransactionUpdatesTimestamp": 0,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },

View File

@@ -4,7 +4,9 @@
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.001",
"unitFees": [
{ "timestamp": 0, "fee": "0.001" }
],
"nameRegistrationUnitFees": [
{ "timestamp": 0, "fee": "1.25" }
],