diff --git a/AutoUpdates.md b/AutoUpdates.md index 7f248246..9e3c618c 100644 --- a/AutoUpdates.md +++ b/AutoUpdates.md @@ -8,7 +8,7 @@ * Build auto-update download: `tools/build-auto-update.sh` - uploads auto-update file into new git branch * Restart local node * Publish auto-update transaction using *private key* for **non-admin** member of "dev" group: - `tools/publish-auto-update.sh non-admin-dev-member-private-key-in-base58` + `tools/publish-auto-update.pl non-admin-dev-member-private-key-in-base58` * Wait for auto-update `ARBITRARY` transaction to be confirmed into a block * Have "dev" group admins 'approve' auto-update using `tools/approve-auto-update.sh` This tool will prompt for *private key* of **admin** of "dev" group diff --git a/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/maven-metadata-local.xml b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 00000000..111dbbcc --- /dev/null +++ b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,24 @@ + + + io.reticulum + reticulum-network-stack + 1.0-SNAPSHOT + + + true + + 20240324170649 + + + jar + 1.0-SNAPSHOT + 20240324170649 + + + pom + 1.0-SNAPSHOT + 20240324170649 + + + + diff --git a/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.jar b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.jar new file mode 100644 index 00000000..7612e6ad Binary files /dev/null and b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.jar differ diff --git a/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.pom b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.pom new file mode 100644 index 00000000..1b1cc206 --- /dev/null +++ b/lib/io/reticulum/reticulum-network-stack/1.0-SNAPSHOT/reticulum-network-stack-1.0-SNAPSHOT.pom @@ -0,0 +1,9 @@ + + + 4.0.0 + io.reticulum + reticulum-network-stack + 1.0-SNAPSHOT + POM was created from install:install-file + diff --git a/lib/io/reticulum/reticulum-network-stack/maven-metadata-local.xml b/lib/io/reticulum/reticulum-network-stack/maven-metadata-local.xml new file mode 100644 index 00000000..6ff327fb --- /dev/null +++ b/lib/io/reticulum/reticulum-network-stack/maven-metadata-local.xml @@ -0,0 +1,11 @@ + + + io.reticulum + reticulum-network-stack + + + 1.0-SNAPSHOT + + 20240324170649 + + diff --git a/lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar b/lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar index 05c548c8..20e773a4 100644 Binary files a/lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar and b/lib/org/ciyam/AT/1.4.1/AT-1.4.1.jar differ diff --git a/lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom b/lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom index 16f644b9..d88a53e2 100644 --- a/lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom +++ b/lib/org/ciyam/AT/1.4.1/AT-1.4.1.pom @@ -10,14 +10,13 @@ UTF-8 false - - 3.8.1 - 3.2.0 - 3.3.1 - 3.0.0-M4 - 3.2.0 - - 1.64 + 1.69 + 4.13.2 + 3.11.0 + 3.3.0 + 3.6.3 + 3.3.0 + 3.2.2 @@ -117,7 +116,7 @@ junit junit - 4.13 + ${junit.version} test diff --git a/lib/org/ciyam/AT/maven-metadata-local.xml b/lib/org/ciyam/AT/maven-metadata-local.xml index d8f3dd34..355c973f 100644 --- a/lib/org/ciyam/AT/maven-metadata-local.xml +++ b/lib/org/ciyam/AT/maven-metadata-local.xml @@ -5,14 +5,11 @@ 1.4.1 - 1.3.4 - 1.3.5 - 1.3.6 1.3.7 1.3.8 1.4.0 1.4.1 - 20230821074325 + 20231212092227 diff --git a/pom.xml b/pom.xml index b64917f4..28061bbb 100644 --- a/pom.xml +++ b/pom.xml @@ -3,59 +3,65 @@ 4.0.0 org.qortal qortal - 4.4.0 + 4.5.1 jar + true 7dc8c6f 0.15.10 1.69 - 3.4.0 + 3.5.0 ${maven.build.timestamp} 1.4.1 3.8.0 1.11.0 2.11.0 - 1.24.0 - 3.13.0 + 1.25.0 + 3.14.0 1.2.2 0.12.3 4.9.10 - 1.59.0 - 32.1.3-jre + 1.61.1 + 33.0.0-jre 2.2 1.2.1 2.5.1 - 74.1 + 74.2 4.12 4.0.1 2.3.9 2.41 - 9.4.53.v20231009 + 9.4.54.v20240208 1.1.1 - 20231013 - 1.16.2 + 20240205 + 1.17.2 5.10.0 1.0.0 - 2.21.1 + 2.22.1 1.5.0-b01 - 3.11.0 + 3.12.1 3.3.0 3.3.1 3.5.1 - 3.2.2 + 3.2.5 1.1.0 UTF-8 - 3.24.4 + 3.25.2 1.5.3 0.16 1.17 1.7.36 2.0.10 - 5.9.0 + 5.10.3 1.2 - 2.16.1 + 2.16.2 1.9 + 1.18.30 + 2.14.3 src/main/java @@ -103,6 +109,14 @@ ${project.build.directory}/swagger-ui.unpacked + @@ -373,6 +387,11 @@ ${maven-surefire-plugin.version} ${skipTests} + + + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + @@ -441,6 +460,10 @@ jitpack.io https://jitpack.io + + true + always + @@ -780,5 +803,95 @@ jaxb-runtime ${jaxb-runtime.version} + + io.reticulum + reticulum-network-stack + 1.0-SNAPSHOT + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.projectlombok + lombok + ${lombok.version} + provided + + + commons-codec + commons-codec + 1.15 + + + org.apache.commons + commons-collections4 + 4.4 + + + org.msgpack + jackson-dataformat-msgpack + 0.9.3 + + + + + + io.netty + netty-all + 4.1.92.Final + + + org.bouncycastle + bcpkix-jdk15on + 1.69 + + + com.macasaet.fernet + fernet-java8 + 1.4.2 + + + org.apache.commons + commons-compress + 1.25.0 + + + com.igormaznitsa + jbbp + 2.0.4 + + + com.github.seancfoley + ipaddress + 5.4.0 + + + org.msgpack + msgpack-core + 0.9.6 + + diff --git a/src/main/java/org/qortal/ApplyBootstrap.java b/src/main/java/org/qortal/ApplyBootstrap.java new file mode 100644 index 00000000..9b370f7a --- /dev/null +++ b/src/main/java/org/qortal/ApplyBootstrap.java @@ -0,0 +1,227 @@ +package org.qortal; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.api.ApiKey; +import org.qortal.api.ApiRequest; +import org.qortal.controller.BootstrapNode; +import org.qortal.settings.Settings; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.Security; +import java.util.*; +import java.util.stream.Collectors; + +import static org.qortal.controller.BootstrapNode.AGENTLIB_JVM_HOLDER_ARG; + +public class ApplyBootstrap { + + static { + // This static block will be called before others if using ApplyBootstrap.main() + + // Log into different files for bootstrap - this has to be before LogManger.getLogger() calls + System.setProperty("log4j2.filenameTemplate", "log-apply-bootstrap.txt"); + + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static final Logger LOGGER = LogManager.getLogger(ApplyBootstrap.class); + private static final String JAR_FILENAME = BootstrapNode.JAR_FILENAME; + private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe"; + private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS"; + private static final String JAVA_TOOL_OPTIONS_VALUE = ""; + + private static final long CHECK_INTERVAL = 15 * 1000L; // ms + private static final int MAX_ATTEMPTS = 20; + + public static void main(String[] args) { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + + // Load/check settings, which potentially sets up blockchain config, etc. + if (args.length > 0) + Settings.fileInstance(args[0]); + else + Settings.getInstance(); + + LOGGER.info("Applying bootstrap..."); + + // Shutdown node using API + if (!shutdownNode()) + return; + + // Delete db + deleteDB(); + + // Restart node + restartNode(args); + + LOGGER.info("Bootstrapping..."); + } + + private static boolean shutdownNode() { + String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/"; + LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri)); + + // The /admin/stop endpoint requires an API key, which may or may not be already generated + boolean apiKeyNewlyGenerated = false; + ApiKey apiKey = null; + try { + apiKey = new ApiKey(); + if (!apiKey.generated()) { + apiKey.generate(); + apiKeyNewlyGenerated = true; + LOGGER.info("Generated API key"); + } + } catch (IOException e) { + LOGGER.info("Error loading API key: {}", e.getMessage()); + } + + // Create GET params + Map params = new HashMap<>(); + if (apiKey != null) { + params.put("apiKey", apiKey.toString()); + } + + // Attempt to stop the node + int attempt; + for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) { + final int attemptForLogging = attempt; + LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS)); + String response = ApiRequest.perform(baseUri + "admin/stop", params); + if (response == null) { + // No response - consider node shut down + if (apiKeyNewlyGenerated) { + // API key was newly generated for bootstrapping node, so we need to remove it + ApplyBootstrap.removeGeneratedApiKey(); + } + return true; + } + + LOGGER.info(() -> String.format("Response from API: %s", response)); + + try { + Thread.sleep(CHECK_INTERVAL); + } catch (InterruptedException e) { + // We still need to check... + break; + } + } + + if (apiKeyNewlyGenerated) { + // API key was newly generated for bootstrapping node, so we need to remove it + ApplyBootstrap.removeGeneratedApiKey(); + } + + if (attempt == MAX_ATTEMPTS) { + LOGGER.error("Failed to shutdown node - giving up"); + return false; + } + + return true; + } + + private static void removeGeneratedApiKey() { + try { + LOGGER.info("Removing newly generated API key..."); + + // Delete the API key since it was only generated for bootstrapping node + ApiKey apiKey = new ApiKey(); + apiKey.delete(); + + } catch (IOException e) { + LOGGER.info("Error loading or deleting API key: {}", e.getMessage()); + } + } + + private static void deleteDB() { + // Get the repository path from settings + String repositoryPath = Settings.getInstance().getRepositoryPath(); + LOGGER.debug(String.format("Repository path: %s", repositoryPath)); + + try { + Path directory = Paths.get(repositoryPath); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + LOGGER.error("Error deleting DB: {}", e.getMessage()); + } + } + + private static void restartNode(String[] args) { + String javaHome = System.getProperty("java.home"); + LOGGER.debug(() -> String.format("Java home: %s", javaHome)); + + Path javaBinary = Paths.get(javaHome, "bin", "java"); + LOGGER.debug(() -> String.format("Java binary: %s", javaBinary)); + + Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER); + LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher)); + + List javaCmd; + if (Files.exists(exeLauncher)) { + javaCmd = Arrays.asList(exeLauncher.toString()); + } else { + javaCmd = new ArrayList<>(); + // Java runtime binary itself + javaCmd.add(javaBinary.toString()); + + // JVM arguments + javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + + // Reapply any retained, but disabled, -agentlib JVM arg + javaCmd = javaCmd.stream() + .map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib")) + .collect(Collectors.toList()); + + // Call mainClass in JAR + javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME)); + + // Add saved command-line args + javaCmd.addAll(Arrays.asList(args)); + } + + try { + LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + if (Files.exists(exeLauncher)) { + LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE)); + processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE); + } + + // New process will inherit our stdout and stderr + processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = processBuilder.start(); + + // Nothing to pipe to new process, so close output stream (process's stdin) + process.getOutputStream().close(); + } catch (Exception e) { + LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/ApplyRestart.java b/src/main/java/org/qortal/ApplyRestart.java new file mode 100644 index 00000000..70d07df5 --- /dev/null +++ b/src/main/java/org/qortal/ApplyRestart.java @@ -0,0 +1,196 @@ +package org.qortal; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.api.ApiKey; +import org.qortal.api.ApiRequest; +import org.qortal.controller.RestartNode; +import org.qortal.settings.Settings; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.Security; +import java.util.*; +import java.util.stream.Collectors; + +import static org.qortal.controller.RestartNode.AGENTLIB_JVM_HOLDER_ARG; + +public class ApplyRestart { + + static { + // This static block will be called before others if using ApplyRestart.main() + + // Log into different files for restart node - this has to be before LogManger.getLogger() calls + System.setProperty("log4j2.filenameTemplate", "log-apply-restart.txt"); + + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static final Logger LOGGER = LogManager.getLogger(ApplyRestart.class); + private static final String JAR_FILENAME = RestartNode.JAR_FILENAME; + private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe"; + private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS"; + private static final String JAVA_TOOL_OPTIONS_VALUE = ""; + + private static final long CHECK_INTERVAL = 10 * 1000L; // ms + private static final int MAX_ATTEMPTS = 12; + + public static void main(String[] args) { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + + // Load/check settings, which potentially sets up blockchain config, etc. + if (args.length > 0) + Settings.fileInstance(args[0]); + else + Settings.getInstance(); + + LOGGER.info("Applying restart..."); + + // Shutdown node using API + if (!shutdownNode()) + return; + + // Restart node + restartNode(args); + + LOGGER.info("Restarting..."); + } + + private static boolean shutdownNode() { + String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/"; + LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri)); + + // The /admin/stop endpoint requires an API key, which may or may not be already generated + boolean apiKeyNewlyGenerated = false; + ApiKey apiKey = null; + try { + apiKey = new ApiKey(); + if (!apiKey.generated()) { + apiKey.generate(); + apiKeyNewlyGenerated = true; + LOGGER.info("Generated API key"); + } + } catch (IOException e) { + LOGGER.info("Error loading API key: {}", e.getMessage()); + } + + // Create GET params + Map params = new HashMap<>(); + if (apiKey != null) { + params.put("apiKey", apiKey.toString()); + } + + // Attempt to stop the node + int attempt; + for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) { + final int attemptForLogging = attempt; + LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS)); + String response = ApiRequest.perform(baseUri + "admin/stop", params); + if (response == null) { + // No response - consider node shut down + if (apiKeyNewlyGenerated) { + // API key was newly generated for restarting node, so we need to remove it + ApplyRestart.removeGeneratedApiKey(); + } + return true; + } + + LOGGER.info(() -> String.format("Response from API: %s", response)); + + try { + Thread.sleep(CHECK_INTERVAL); + } catch (InterruptedException e) { + // We still need to check... + break; + } + } + + if (apiKeyNewlyGenerated) { + // API key was newly generated for restarting node, so we need to remove it + ApplyRestart.removeGeneratedApiKey(); + } + + if (attempt == MAX_ATTEMPTS) { + LOGGER.error("Failed to shutdown node - giving up"); + return false; + } + + return true; + } + + private static void removeGeneratedApiKey() { + try { + LOGGER.info("Removing newly generated API key..."); + + // Delete the API key since it was only generated for restarting node + ApiKey apiKey = new ApiKey(); + apiKey.delete(); + + } catch (IOException e) { + LOGGER.info("Error loading or deleting API key: {}", e.getMessage()); + } + } + + private static void restartNode(String[] args) { + String javaHome = System.getProperty("java.home"); + LOGGER.debug(() -> String.format("Java home: %s", javaHome)); + + Path javaBinary = Paths.get(javaHome, "bin", "java"); + LOGGER.debug(() -> String.format("Java binary: %s", javaBinary)); + + Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER); + LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher)); + + List javaCmd; + if (Files.exists(exeLauncher)) { + javaCmd = Arrays.asList(exeLauncher.toString()); + } else { + javaCmd = new ArrayList<>(); + // Java runtime binary itself + javaCmd.add(javaBinary.toString()); + + // JVM arguments + javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + + // Reapply any retained, but disabled, -agentlib JVM arg + javaCmd = javaCmd.stream() + .map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib")) + .collect(Collectors.toList()); + + // Call mainClass in JAR + javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME)); + + // Add saved command-line args + javaCmd.addAll(Arrays.asList(args)); + } + + try { + LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + if (Files.exists(exeLauncher)) { + LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE)); + processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE); + } + + // New process will inherit our stdout and stderr + processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = processBuilder.start(); + + // Nothing to pipe to new process, so close output stream (process's stdin) + process.getOutputStream().close(); + } catch (Exception e) { + LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/ApplyUpdate.java b/src/main/java/org/qortal/ApplyUpdate.java index f1f9d22f..ba2e0860 100644 --- a/src/main/java/org/qortal/ApplyUpdate.java +++ b/src/main/java/org/qortal/ApplyUpdate.java @@ -38,7 +38,7 @@ public class ApplyUpdate { private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME; private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe"; private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS"; - private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4"; + private static final String JAVA_TOOL_OPTIONS_VALUE = ""; private static final long CHECK_INTERVAL = 30 * 1000L; // ms private static final int MAX_ATTEMPTS = 12; @@ -139,7 +139,7 @@ public class ApplyUpdate { apiKey.delete(); } catch (IOException e) { - LOGGER.info("Error loading or deleting API key: {}", e.getMessage()); + LOGGER.error("Error loading or deleting API key: {}", e.getMessage()); } } @@ -181,13 +181,13 @@ public class ApplyUpdate { private static void restartNode(String[] args) { String javaHome = System.getProperty("java.home"); - LOGGER.info(() -> String.format("Java home: %s", javaHome)); + LOGGER.debug(() -> String.format("Java home: %s", javaHome)); Path javaBinary = Paths.get(javaHome, "bin", "java"); - LOGGER.info(() -> String.format("Java binary: %s", javaBinary)); + LOGGER.debug(() -> String.format("Java binary: %s", javaBinary)); Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER); - LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher)); + LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher)); List javaCmd; if (Files.exists(exeLauncher)) { @@ -213,12 +213,12 @@ public class ApplyUpdate { } try { - LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd))); ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); if (Files.exists(exeLauncher)) { - LOGGER.info(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE)); + LOGGER.debug(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE)); processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE); } diff --git a/src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java b/src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java index 725e53f5..65092602 100644 --- a/src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java +++ b/src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java @@ -363,5 +363,4 @@ public class SelfSponsorshipAlgoV1 { return transactionDataList; } - -} +} \ No newline at end of file diff --git a/src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java b/src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java new file mode 100644 index 00000000..9052257c --- /dev/null +++ b/src/main/java/org/qortal/account/SelfSponsorshipAlgoV2.java @@ -0,0 +1,249 @@ +package org.qortal.account; + +import org.qortal.api.resource.TransactionsResource; +import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; +import org.qortal.data.account.AccountData; +import org.qortal.data.transaction.*; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.transaction.Transaction.TransactionType; + +import java.util.*; + +public class SelfSponsorshipAlgoV2 { + + private final long snapshotTimestampV1 = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp(); + private final long snapshotTimestampV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2SnapshotTimestamp(); + private final long referenceTimestamp = BlockChain.getInstance().getReferenceTimestampBlock(); + + private final boolean override; + private final Repository repository; + private final String address; + + private int recentAssetSendCount = 0; + private int recentSponsorshipCount = 0; + + private final Set assetAddresses = new LinkedHashSet<>(); + private final Set penaltyAddresses = new LinkedHashSet<>(); + private final Set sponsorAddresses = new LinkedHashSet<>(); + + private List sponsorshipRewardShares = new ArrayList<>(); + private List transferAssetForAddress = new ArrayList<>(); + + public SelfSponsorshipAlgoV2(Repository repository, String address, boolean override) { + this.repository = repository; + this.address = address; + this.override = override; + } + + public String getAddress() { + return this.address; + } + + public Set getPenaltyAddresses() { + return this.penaltyAddresses; + } + + public void run() throws DataException { + if (!override) { + this.getAccountPrivs(this.address); + } + + if (override) { + this.fetchTransferAssetForAddress(this.address); + this.findRecentAssetSendCount(); + + if (this.recentAssetSendCount >= 6) { + this.penaltyAddresses.add(this.address); + this.penaltyAddresses.addAll(this.assetAddresses); + } + } + } + + private void getAccountPrivs(String address) throws DataException { + AccountData accountData = this.repository.getAccountRepository().getAccount(address); + List transferPrivsTransactions = fetchTransferPrivsForAddress(address); + transferPrivsTransactions.removeIf(t -> t.getTimestamp() > this.referenceTimestamp || accountData.getAddress().equals(t.getRecipient())); + + if (transferPrivsTransactions.isEmpty()) { + // Nothing to do + return; + } + + for (TransactionData transactionData : transferPrivsTransactions) { + TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData; + this.penaltyAddresses.add(transferPrivsTransactionData.getRecipient()); + this.fetchSponsorshipRewardShares(transferPrivsTransactionData.getRecipient()); + this.findRecentSponsorshipCount(); + + if (this.recentSponsorshipCount >= 1) { + this.penaltyAddresses.addAll(this.sponsorAddresses); + } + + String newAddress = this.getDestinationAccount(transferPrivsTransactionData.getRecipient()); + + while (newAddress != null) { + // Found destination account + this.penaltyAddresses.add(newAddress); + this.fetchSponsorshipRewardShares(newAddress); + this.findRecentSponsorshipCount(); + + if (this.recentSponsorshipCount >= 1) { + this.penaltyAddresses.addAll(this.sponsorAddresses); + } + + newAddress = this.getDestinationAccount(newAddress); + } + } + } + + private String getDestinationAccount(String address) throws DataException { + AccountData accountData = this.repository.getAccountRepository().getAccount(address); + List transferPrivsTransactions = fetchTransferPrivsForAddress(address); + transferPrivsTransactions.removeIf(t -> t.getTimestamp() > this.referenceTimestamp || accountData.getAddress().equals(t.getRecipient())); + + if (transferPrivsTransactions.isEmpty()) { + return null; + } + + if (accountData == null) { + return null; + } + + for (TransactionData transactionData : transferPrivsTransactions) { + TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData; + if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) { + return transferPrivsTransactionData.getRecipient(); + } + } + + return null; + } + + private void fetchSponsorshipRewardShares(String address) throws DataException { + AccountData accountDataRs = this.repository.getAccountRepository().getAccount(address); + List sponsorshipRewardShares = new ArrayList<>(); + + // Define relevant transactions + List txTypes = List.of(TransactionType.REWARD_SHARE); + List transactionDataList = fetchTransactions(repository, txTypes, address, false); + + for (TransactionData transactionData : transactionDataList) { + if (transactionData.getType() != TransactionType.REWARD_SHARE) { + continue; + } + + RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData; + + // Skip removals + if (rewardShareTransactionData.getSharePercent() < 0) { + continue; + } + + // Skip if not sponsored by this account + if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountDataRs.getPublicKey())) { + continue; + } + + // Skip self shares + if (Objects.equals(rewardShareTransactionData.getRecipient(), address)) { + continue; + } + + boolean duplicateFound = false; + for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) { + if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) { + // Duplicate + duplicateFound = true; + break; + } + } + + if (!duplicateFound) { + sponsorshipRewardShares.add(rewardShareTransactionData); + this.sponsorAddresses.add(rewardShareTransactionData.getRecipient()); + } + } + + this.sponsorshipRewardShares = sponsorshipRewardShares; + } + + private void fetchTransferAssetForAddress(String address) throws DataException { + List transferAssetForAddress = new ArrayList<>(); + + // Define relevant transactions + List txTypes = List.of(TransactionType.TRANSFER_ASSET); + List transactionDataList = fetchTransactions(repository, txTypes, address, false); + transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV2); + + for (TransactionData transactionData : transactionDataList) { + if (transactionData.getType() != TransactionType.TRANSFER_ASSET) { + continue; + } + + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData; + + if (transferAssetTransactionData.getAssetId() == Asset.QORT) { + if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) { + // Outgoing transfer asset for this account + transferAssetForAddress.add(transferAssetTransactionData); + this.assetAddresses.add(transferAssetTransactionData.getRecipient()); + } + } + } + + this.transferAssetForAddress = transferAssetForAddress; + } + + private void findRecentSponsorshipCount() { + int recentSponsorshipCount = 0; + + for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) { + if (rewardShare.getTimestamp() >= this.snapshotTimestampV1) { + recentSponsorshipCount++; + } + } + + this.recentSponsorshipCount = recentSponsorshipCount; + } + + private void findRecentAssetSendCount() { + int recentAssetSendCount = 0; + + for (TransferAssetTransactionData assetSend : transferAssetForAddress) { + if (assetSend.getTimestamp() >= this.snapshotTimestampV1) { + recentAssetSendCount++; + } + } + + this.recentAssetSendCount = recentAssetSendCount; + } + + private List fetchTransferPrivsForAddress(String address) throws DataException { + return fetchTransactions(repository, + List.of(TransactionType.TRANSFER_PRIVS), + address, true); + } + + private static List fetchTransactions(Repository repository, List txTypes, String address, boolean reverse) throws DataException { + // Fetch all relevant transactions for this account + List signatures = repository.getTransactionRepository() + .getSignaturesMatchingCriteria(null, null, null, txTypes, + null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, + null, null, reverse); + + List transactionDataList = new ArrayList<>(); + + for (byte[] signature : signatures) { + // Fetch transaction data + TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); + if (transactionData == null) { + continue; + } + transactionDataList.add(transactionData); + } + + return transactionDataList; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java b/src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java new file mode 100644 index 00000000..80c1ba2f --- /dev/null +++ b/src/main/java/org/qortal/account/SelfSponsorshipAlgoV3.java @@ -0,0 +1,370 @@ +package org.qortal.account; + +import org.qortal.api.resource.TransactionsResource; +import org.qortal.asset.Asset; +import org.qortal.data.account.AccountData; +import org.qortal.data.naming.NameData; +import org.qortal.data.transaction.*; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.transaction.Transaction.TransactionType; + +import java.util.*; +import java.util.stream.Collectors; + +public class SelfSponsorshipAlgoV3 { + + private final Repository repository; + private final String address; + private final AccountData accountData; + private final long snapshotTimestampV1; + private final long snapshotTimestampV3; + private final boolean override; + + private int registeredNameCount = 0; + private int suspiciousCount = 0; + private int suspiciousPercent = 0; + private int consolidationCount = 0; + private int bulkIssuanceCount = 0; + private int recentSponsorshipCount = 0; + + private List sponsorshipRewardShares = new ArrayList<>(); + private final Map> paymentsByAddress = new HashMap<>(); + private final Set sponsees = new LinkedHashSet<>(); + private Set consolidatedAddresses = new LinkedHashSet<>(); + private final Set zeroTransactionAddreses = new LinkedHashSet<>(); + private final Set penaltyAddresses = new LinkedHashSet<>(); + + public SelfSponsorshipAlgoV3(Repository repository, String address, long snapshotTimestampV1, long snapshotTimestampV3, boolean override) throws DataException { + this.repository = repository; + this.address = address; + this.accountData = this.repository.getAccountRepository().getAccount(this.address); + this.snapshotTimestampV1 = snapshotTimestampV1; + this.snapshotTimestampV3 = snapshotTimestampV3; + this.override = override; + } + + public String getAddress() { + return this.address; + } + + public Set getPenaltyAddresses() { + return this.penaltyAddresses; + } + + + public void run() throws DataException { + if (this.accountData == null) { + // Nothing to do + return; + } + + this.fetchSponsorshipRewardShares(); + if (this.sponsorshipRewardShares.isEmpty()) { + // Nothing to do + return; + } + + this.findConsolidatedRewards(); + this.findBulkIssuance(); + this.findRegisteredNameCount(); + this.findRecentSponsorshipCount(); + + int score = this.calculateScore(); + if (score <= 0 && !override) { + return; + } + + String newAddress = this.getDestinationAccount(this.address); + while (newAddress != null) { + // Found destination account + this.penaltyAddresses.add(newAddress); + + // Run algo for this address, but in "override" mode because it has already been flagged + SelfSponsorshipAlgoV3 algoV3 = new SelfSponsorshipAlgoV3(this.repository, newAddress, this.snapshotTimestampV1, this.snapshotTimestampV3, true); + algoV3.run(); + this.penaltyAddresses.addAll(algoV3.getPenaltyAddresses()); + + newAddress = this.getDestinationAccount(newAddress); + } + + this.penaltyAddresses.add(this.address); + + if (this.override || this.recentSponsorshipCount < 20) { + this.penaltyAddresses.addAll(this.consolidatedAddresses); + this.penaltyAddresses.addAll(this.zeroTransactionAddreses); + } + else { + this.penaltyAddresses.addAll(this.sponsees); + } + } + + private String getDestinationAccount(String address) throws DataException { + List transferPrivsTransactions = fetchTransferPrivsForAddress(address); + if (transferPrivsTransactions.isEmpty()) { + // No TRANSFER_PRIVS transactions for this address + return null; + } + + AccountData accountData = this.repository.getAccountRepository().getAccount(address); + if (accountData == null) { + return null; + } + + for (TransactionData transactionData : transferPrivsTransactions) { + TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData; + if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) { + return transferPrivsTransactionData.getRecipient(); + } + } + + return null; + } + + private void findConsolidatedRewards() throws DataException { + List sponseesThatSentRewards = new ArrayList<>(); + Map paymentRecipients = new HashMap<>(); + + // Collect outgoing payments of each sponsee + for (String sponseeAddress : this.sponsees) { + + // Firstly fetch all payments for address, since the functions below depend on this data + this.fetchPaymentsForAddress(sponseeAddress); + + // Check if the address has zero relevant transactions + if (this.hasZeroTransactions(sponseeAddress)) { + this.zeroTransactionAddreses.add(sponseeAddress); + } + + // Get payment recipients + List allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress); + if (allPaymentRecipients.isEmpty()) { + continue; + } + sponseesThatSentRewards.add(sponseeAddress); + + List addressesPaidByThisSponsee = new ArrayList<>(); + for (String paymentRecipient : allPaymentRecipients) { + if (addressesPaidByThisSponsee.contains(paymentRecipient)) { + // We already tracked this association - don't allow multiple to stack up + continue; + } + addressesPaidByThisSponsee.add(paymentRecipient); + + // Increment count for this recipient, or initialize to 1 if not present + if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) { + paymentRecipients.put(paymentRecipient, 1); + } + } + + } + + // Exclude addresses with a low number of payments + Map filteredPaymentRecipients = paymentRecipients.entrySet().stream() + .filter(p -> p.getValue() != null && p.getValue() >= 10) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + // Now check how many sponsees have sent to this subset of addresses + Map sponseesThatConsolidatedRewards = new HashMap<>(); + for (String sponseeAddress : sponseesThatSentRewards) { + List allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress); + // Remove any that aren't to one of the flagged recipients (i.e. consolidation) + allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r)); + + int count = allPaymentRecipients.size(); + if (count == 0) { + continue; + } + if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) { + sponseesThatConsolidatedRewards.put(sponseeAddress, count); + } + } + + // Remove sponsees that have only sent a low number of payments to the filtered addresses + Map filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream() + .filter(p -> p.getValue() != null && p.getValue() >= 2) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + this.consolidationCount = sponseesThatConsolidatedRewards.size(); + this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet()); + this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size(); + this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100); + } + + private void findBulkIssuance() { + Long lastTimestamp = null; + for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) { + long timestamp = rewardShareTransactionData.getTimestamp(); + if (timestamp >= this.snapshotTimestampV3) { + continue; + } + + if (lastTimestamp != null) { + if (timestamp - lastTimestamp < 3*60*1000L) { + this.bulkIssuanceCount++; + } + } + lastTimestamp = timestamp; + } + } + + private void findRegisteredNameCount() throws DataException { + int registeredNameCount = 0; + for (String sponseeAddress : sponsees) { + List names = repository.getNameRepository().getNamesByOwner(sponseeAddress); + for (NameData name : names) { + if (name.getRegistered() < this.snapshotTimestampV3) { + registeredNameCount++; + break; + } + } + } + this.registeredNameCount = registeredNameCount; + } + + private void findRecentSponsorshipCount() { + int recentSponsorshipCount = 0; + for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) { + if (rewardShare.getTimestamp() >= this.snapshotTimestampV1) { + recentSponsorshipCount++; + } + } + this.recentSponsorshipCount = recentSponsorshipCount; + } + + private int calculateScore() { + final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1; + final int nameMultiplier = (this.sponsees.size() >= 25 && this.registeredNameCount <= 1) ? 21 : + (this.sponsees.size() >= 15 && this.registeredNameCount <= 1) ? 11 : + (this.sponsees.size() >= 5 && this.registeredNameCount <= 1) ? 5 : 1; + final int consolidationMultiplier = Math.max(this.consolidationCount, 1); + final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1); + final int offset = 20; + return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset; + } + + private void fetchSponsorshipRewardShares() throws DataException { + List sponsorshipRewardShares = new ArrayList<>(); + + // Define relevant transactions + List txTypes = List.of(TransactionType.REWARD_SHARE); + List transactionDataList = fetchTransactions(repository, txTypes, this.address, false); + transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3); + + for (TransactionData transactionData : transactionDataList) { + if (transactionData.getType() != TransactionType.REWARD_SHARE) { + continue; + } + + RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData; + + // Skip removals + if (rewardShareTransactionData.getSharePercent() < 0) { + continue; + } + + // Skip if not sponsored by this account + if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) { + continue; + } + + // Skip self shares + if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) { + continue; + } + + boolean duplicateFound = false; + for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) { + if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) { + // Duplicate + duplicateFound = true; + break; + } + } + if (!duplicateFound) { + sponsorshipRewardShares.add(rewardShareTransactionData); + this.sponsees.add(rewardShareTransactionData.getRecipient()); + } + } + + this.sponsorshipRewardShares = sponsorshipRewardShares; + } + + private List fetchTransferPrivsForAddress(String address) throws DataException { + return fetchTransactions(repository, + List.of(TransactionType.TRANSFER_PRIVS), + address, true); + } + + private void fetchPaymentsForAddress(String address) throws DataException { + List payments = fetchTransactions(repository, + Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET), + address, false); + this.paymentsByAddress.put(address, payments); + } + + private List fetchOutgoingPaymentRecipientsForAddress(String address) { + List outgoingPaymentRecipients = new ArrayList<>(); + + List transactionDataList = this.paymentsByAddress.get(address); + if (transactionDataList == null) transactionDataList = new ArrayList<>(); + transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3); + for (TransactionData transactionData : transactionDataList) { + switch (transactionData.getType()) { + + case PAYMENT: + PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData; + if (!Objects.equals(paymentTransactionData.getRecipient(), address)) { + // Outgoing payment from this account + outgoingPaymentRecipients.add(paymentTransactionData.getRecipient()); + } + break; + + case TRANSFER_ASSET: + TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData; + if (transferAssetTransactionData.getAssetId() == Asset.QORT) { + if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) { + // Outgoing payment from this account + outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient()); + } + } + break; + + default: + break; + } + } + + return outgoingPaymentRecipients; + } + + private boolean hasZeroTransactions(String address) { + List transactionDataList = this.paymentsByAddress.get(address); + if (transactionDataList == null) { + return true; + } + transactionDataList.removeIf(t -> t.getTimestamp() <= this.snapshotTimestampV1 || t.getTimestamp() >= this.snapshotTimestampV3); + return transactionDataList.size() == 0; + } + + private static List fetchTransactions(Repository repository, List txTypes, String address, boolean reverse) throws DataException { + // Fetch all relevant transactions for this account + List signatures = repository.getTransactionRepository() + .getSignaturesMatchingCriteria(null, null, null, txTypes, + null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, + null, null, reverse); + + List transactionDataList = new ArrayList<>(); + + for (byte[] signature : signatures) { + // Fetch transaction data + TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); + if (transactionData == null) { + continue; + } + transactionDataList.add(transactionData); + } + + return transactionDataList; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/model/PollVotes.java b/src/main/java/org/qortal/api/model/PollVotes.java index 2768694f..49191be2 100644 --- a/src/main/java/org/qortal/api/model/PollVotes.java +++ b/src/main/java/org/qortal/api/model/PollVotes.java @@ -20,17 +20,25 @@ public class PollVotes { @Schema(description = "Total number of votes") public Integer totalVotes; + @Schema(description = "Total weight of votes") + public Integer totalWeight; + @Schema(description = "List of vote counts for each option") public List voteCounts; + @Schema(description = "List of vote weights for each option") + public List voteWeights; + // For JAX-RS protected PollVotes() { } - public PollVotes(List votes, Integer totalVotes, List voteCounts) { + public PollVotes(List votes, Integer totalVotes, Integer totalWeight, List voteCounts, List voteWeights) { this.votes = votes; this.totalVotes = totalVotes; + this.totalWeight = totalWeight; this.voteCounts = voteCounts; + this.voteWeights = voteWeights; } @Schema(description = "Vote info") @@ -52,4 +60,24 @@ public class PollVotes { this.voteCount = voteCount; } } + + @Schema(description = "Vote weights") + // All properties to be converted to JSON via JAX-RS + @XmlAccessorType(XmlAccessType.FIELD) + public static class OptionWeight { + @Schema(description = "Option name") + public String optionName; + + @Schema(description = "Vote weight") + public Integer voteWeight; + + // For JAX-RS + protected OptionWeight() { + } + + public OptionWeight(String optionName, Integer voteWeight) { + this.optionName = optionName; + this.voteWeight = voteWeight; + } + } } diff --git a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java index 88cd622d..ff4aee02 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.qortal.api.ApiError; import org.qortal.api.ApiErrors; @@ -19,6 +20,7 @@ import org.qortal.crosschain.AddressInfo; import org.qortal.crosschain.Bitcoin; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.SimpleTransaction; +import org.qortal.crosschain.ServerConfigurationInfo; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; @@ -188,45 +190,6 @@ public class CrossChainBitcoinResource { } } - @POST - @Path("/unusedaddress") - @Operation( - summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet", - description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet", - requestBody = @RequestBody( - required = true, - content = @Content( - mediaType = MediaType.TEXT_PLAIN, - schema = @Schema( - type = "string", - description = "BIP32 'm' private/public key in base58", - example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc" - ) - ) - ), - responses = { - @ApiResponse( - content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) ) - ) - } - ) - @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE}) - @SecurityRequirement(name = "apiKey") - public String getUnusedBitcoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) { - Security.checkApiCallAllowed(request); - - Bitcoin bitcoin = Bitcoin.getInstance(); - - if (!bitcoin.isValidDeterministicKey(key58)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); - - try { - return bitcoin.getUnusedReceiveAddress(key58); - } catch (ForeignBlockchainException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); - } - } - @POST @Path("/send") @Operation( @@ -283,4 +246,137 @@ public class CrossChainBitcoinResource { return spendTransaction.getTxId().toString(); } + @GET + @Path("/serverinfos") + @Operation( + summary = "Returns current Bitcoin server configuration", + description = "Returns current Bitcoin server locations and use status", + responses = { + @ApiResponse( + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConfigurationInfo.class + ) + ) + ) + } + ) + public ServerConfigurationInfo getServerConfiguration() { + + return CrossChainUtils.buildServerConfigurationInfo(Bitcoin.getInstance()); + } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Bitcoin fee per Kb.", + description = "Returns Bitcoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getBitcoinFeePerKb() { + Bitcoin bitcoin = Bitcoin.getInstance(); + + return String.valueOf(bitcoin.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Bitcoin fee per Kb.", + description = "Sets Bitcoin fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setBitcoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Bitcoin bitcoin = Bitcoin.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(bitcoin, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Bitcoin fee per Kb.", + description = "Returns Bitcoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getBitcoinFeeCeiling() { + Bitcoin bitcoin = Bitcoin.getInstance(); + + return String.valueOf(bitcoin.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Bitcoin fee ceiling.", + description = "Sets Bitcoin fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setBitcoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Bitcoin bitcoin = Bitcoin.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(bitcoin, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } } diff --git a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java index 3ab944ab..d78a4ed9 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainDigibyteResource.java @@ -19,6 +19,7 @@ import org.qortal.crosschain.AddressInfo; import org.qortal.crosschain.Digibyte; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.SimpleTransaction; +import org.qortal.crosschain.ServerConfigurationInfo; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; @@ -188,45 +189,6 @@ public class CrossChainDigibyteResource { } } - @POST - @Path("/unusedaddress") - @Operation( - summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet", - description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet", - requestBody = @RequestBody( - required = true, - content = @Content( - mediaType = MediaType.TEXT_PLAIN, - schema = @Schema( - type = "string", - description = "BIP32 'm' private/public key in base58", - example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc" - ) - ) - ), - responses = { - @ApiResponse( - content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) ) - ) - } - ) - @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE}) - @SecurityRequirement(name = "apiKey") - public String getUnusedDigibyteReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) { - Security.checkApiCallAllowed(request); - - Digibyte digibyte = Digibyte.getInstance(); - - if (!digibyte.isValidDeterministicKey(key58)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); - - try { - return digibyte.getUnusedReceiveAddress(key58); - } catch (ForeignBlockchainException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); - } - } - @POST @Path("/send") @Operation( @@ -283,4 +245,137 @@ public class CrossChainDigibyteResource { return spendTransaction.getTxId().toString(); } -} + @GET + @Path("/serverinfos") + @Operation( + summary = "Returns current Digibyte server configuration", + description = "Returns current Digibyte server locations and use status", + responses = { + @ApiResponse( + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConfigurationInfo.class + ) + ) + ) + } + ) + public ServerConfigurationInfo getServerConfiguration() { + + return CrossChainUtils.buildServerConfigurationInfo(Digibyte.getInstance()); + } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Digibyte fee per Kb.", + description = "Returns Digibyte fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getDigibyteFeePerKb() { + Digibyte digibyte = Digibyte.getInstance(); + + return String.valueOf(digibyte.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Digibyte fee per Kb.", + description = "Sets Digibyte fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setDigibyteFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Digibyte digibyte = Digibyte.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(digibyte, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Digibyte fee per Kb.", + description = "Returns Digibyte fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getDigibyteFeeCeiling() { + Digibyte digibyte = Digibyte.getInstance(); + + return String.valueOf(digibyte.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Digibyte fee ceiling.", + description = "Sets Digibyte fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setDigibyteFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Digibyte digibyte = Digibyte.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(digibyte, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java index b66b5716..8575a28d 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainDogecoinResource.java @@ -19,6 +19,7 @@ import org.qortal.crosschain.AddressInfo; import org.qortal.crosschain.Dogecoin; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.SimpleTransaction; +import org.qortal.crosschain.ServerConfigurationInfo; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; @@ -188,45 +189,6 @@ public class CrossChainDogecoinResource { } } - @POST - @Path("/unusedaddress") - @Operation( - summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet", - description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet", - requestBody = @RequestBody( - required = true, - content = @Content( - mediaType = MediaType.TEXT_PLAIN, - schema = @Schema( - type = "string", - description = "BIP32 'm' private/public key in base58", - example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc" - ) - ) - ), - responses = { - @ApiResponse( - content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) ) - ) - } - ) - @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE}) - @SecurityRequirement(name = "apiKey") - public String getUnusedDogecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) { - Security.checkApiCallAllowed(request); - - Dogecoin dogecoin = Dogecoin.getInstance(); - - if (!dogecoin.isValidDeterministicKey(key58)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); - - try { - return dogecoin.getUnusedReceiveAddress(key58); - } catch (ForeignBlockchainException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); - } - } - @POST @Path("/send") @Operation( @@ -283,4 +245,137 @@ public class CrossChainDogecoinResource { return spendTransaction.getTxId().toString(); } -} + @GET + @Path("/serverinfos") + @Operation( + summary = "Returns current Dogecoin server configuration", + description = "Returns current Dogecoin server locations and use status", + responses = { + @ApiResponse( + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConfigurationInfo.class + ) + ) + ) + } + ) + public ServerConfigurationInfo getServerConfiguration() { + + return CrossChainUtils.buildServerConfigurationInfo(Dogecoin.getInstance()); + } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Dogecoin fee per Kb.", + description = "Returns Dogecoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getDogecoinFeePerKb() { + Dogecoin dogecoin = Dogecoin.getInstance(); + + return String.valueOf(dogecoin.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Dogecoin fee per Kb.", + description = "Sets Dogecoin fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setDogecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Dogecoin dogecoin = Dogecoin.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(dogecoin, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Dogecoin fee per Kb.", + description = "Returns Dogecoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getDogecoinFeeCeiling() { + Dogecoin dogecoin = Dogecoin.getInstance(); + + return String.valueOf(dogecoin.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Dogecoin fee ceiling.", + description = "Sets Dogecoin fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setDogecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Dogecoin dogecoin = Dogecoin.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(dogecoin, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java index 53274f1e..7667eea1 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java @@ -19,6 +19,7 @@ import org.qortal.crosschain.AddressInfo; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.Litecoin; import org.qortal.crosschain.SimpleTransaction; +import org.qortal.crosschain.ServerConfigurationInfo; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; @@ -188,45 +189,6 @@ public class CrossChainLitecoinResource { } } - @POST - @Path("/unusedaddress") - @Operation( - summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet", - description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet", - requestBody = @RequestBody( - required = true, - content = @Content( - mediaType = MediaType.TEXT_PLAIN, - schema = @Schema( - type = "string", - description = "BIP32 'm' private/public key in base58", - example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc" - ) - ) - ), - responses = { - @ApiResponse( - content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) ) - ) - } - ) - @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE}) - @SecurityRequirement(name = "apiKey") - public String getUnusedLitecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) { - Security.checkApiCallAllowed(request); - - Litecoin litecoin = Litecoin.getInstance(); - - if (!litecoin.isValidDeterministicKey(key58)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); - - try { - return litecoin.getUnusedReceiveAddress(key58); - } catch (ForeignBlockchainException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); - } - } - @POST @Path("/send") @Operation( @@ -283,4 +245,176 @@ public class CrossChainLitecoinResource { return spendTransaction.getTxId().toString(); } -} + @GET + @Path("/serverinfos") + @Operation( + summary = "Returns current Litecoin server configuration", + description = "Returns current Litecoin server locations and use status", + responses = { + @ApiResponse( + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConfigurationInfo.class + ) + ) + ) + } + ) + public ServerConfigurationInfo getServerConfiguration() { + + return CrossChainUtils.buildServerConfigurationInfo(Litecoin.getInstance()); + } + + @POST + @Path("/repair") + @Operation( + summary = "Sends all coins in wallet to primary receive address", + description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string", + description = "BIP32 'm' private/public key in base58", + example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE}) + @SecurityRequirement(name = "apiKey") + public String repairOldWallet(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) { + Security.checkApiCallAllowed(request); + + Litecoin litecoin = Litecoin.getInstance(); + + if (!litecoin.isValidDeterministicKey(key58)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); + + try { + return litecoin.repairOldWallet(key58); + } catch (ForeignBlockchainException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); + } + } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Litecoin fee per Kb.", + description = "Returns Litecoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getLitecoinFeePerKb() { + Litecoin litecoin = Litecoin.getInstance(); + + return String.valueOf(litecoin.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Litecoin fee per Kb.", + description = "Sets Litecoin fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setLitecoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Litecoin litecoin = Litecoin.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(litecoin, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Litecoin fee per Kb.", + description = "Returns Litecoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getLitecoinFeeCeiling() { + Litecoin litecoin = Litecoin.getInstance(); + + return String.valueOf(litecoin.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Litecoin fee ceiling.", + description = "Sets Litecoin fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setLitecoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Litecoin litecoin = Litecoin.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(litecoin, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java index 7f2e8402..03ff43b8 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainPirateChainResource.java @@ -16,6 +16,7 @@ import org.qortal.api.model.crosschain.PirateChainSendRequest; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.PirateChain; import org.qortal.crosschain.SimpleTransaction; +import org.qortal.crosschain.ServerConfigurationInfo; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; @@ -329,4 +330,138 @@ public class CrossChainPirateChainResource { throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage()); } } + + @GET + @Path("/serverinfos") + @Operation( + summary = "Returns current PirateChain server configuration", + description = "Returns current PirateChain server locations and use status", + responses = { + @ApiResponse( + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConfigurationInfo.class + ) + ) + ) + } + ) + public ServerConfigurationInfo getServerConfiguration() { + + return CrossChainUtils.buildServerConfigurationInfo(PirateChain.getInstance()); + } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns PirateChain fee per Kb.", + description = "Returns PirateChain fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getPirateChainFeePerKb() { + PirateChain pirateChain = PirateChain.getInstance(); + + return String.valueOf(pirateChain.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets PirateChain fee per Kb.", + description = "Sets PirateChain fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setPirateChainFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + PirateChain pirateChain = PirateChain.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(pirateChain, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns PirateChain fee per Kb.", + description = "Returns PirateChain fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getPirateChainFeeCeiling() { + PirateChain pirateChain = PirateChain.getInstance(); + + return String.valueOf(pirateChain.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets PirateChain fee ceiling.", + description = "Sets PirateChain fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setPirateChainFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + PirateChain pirateChain = PirateChain.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(pirateChain, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } } diff --git a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java index 4d3f204d..ce5cd668 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainRavencoinResource.java @@ -19,6 +19,7 @@ import org.qortal.crosschain.AddressInfo; import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.Ravencoin; import org.qortal.crosschain.SimpleTransaction; +import org.qortal.crosschain.ServerConfigurationInfo; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; @@ -188,45 +189,6 @@ public class CrossChainRavencoinResource { } } - @POST - @Path("/unusedaddress") - @Operation( - summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet", - description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet", - requestBody = @RequestBody( - required = true, - content = @Content( - mediaType = MediaType.TEXT_PLAIN, - schema = @Schema( - type = "string", - description = "BIP32 'm' private/public key in base58", - example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc" - ) - ) - ), - responses = { - @ApiResponse( - content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) ) - ) - } - ) - @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE}) - @SecurityRequirement(name = "apiKey") - public String getUnusedRavencoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) { - Security.checkApiCallAllowed(request); - - Ravencoin ravencoin = Ravencoin.getInstance(); - - if (!ravencoin.isValidDeterministicKey(key58)) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); - - try { - return ravencoin.getUnusedReceiveAddress(key58); - } catch (ForeignBlockchainException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); - } - } - @POST @Path("/send") @Operation( @@ -283,4 +245,137 @@ public class CrossChainRavencoinResource { return spendTransaction.getTxId().toString(); } + @GET + @Path("/serverinfos") + @Operation( + summary = "Returns current Ravencoin server configuration", + description = "Returns current Ravencoin server locations and use status", + responses = { + @ApiResponse( + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = ServerConfigurationInfo.class + ) + ) + ) + } + ) + public ServerConfigurationInfo getServerConfiguration() { + + return CrossChainUtils.buildServerConfigurationInfo(Ravencoin.getInstance()); + } + + @GET + @Path("/feekb") + @Operation( + summary = "Returns Ravencoin fee per Kb.", + description = "Returns Ravencoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getRavencoinFeePerKb() { + Ravencoin ravencoin = Ravencoin.getInstance(); + + return String.valueOf(ravencoin.getFeePerKb().value); + } + + @POST + @Path("/updatefeekb") + @Operation( + summary = "Sets Ravencoin fee per Kb.", + description = "Sets Ravencoin fee per Kb.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee per Kb", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setRavencoinFeePerKb(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Ravencoin ravencoin = Ravencoin.getInstance(); + + try { + return CrossChainUtils.setFeePerKb(ravencoin, fee); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } + + @GET + @Path("/feeceiling") + @Operation( + summary = "Returns Ravencoin fee per Kb.", + description = "Returns Ravencoin fee per Kb.", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "number" + ) + ) + ) + } + ) + public String getRavencoinFeeCeiling() { + Ravencoin ravencoin = Ravencoin.getInstance(); + + return String.valueOf(ravencoin.getFeeCeiling()); + } + + @POST + @Path("/updatefeeceiling") + @Operation( + summary = "Sets Ravencoin fee ceiling.", + description = "Sets Ravencoin fee ceiling.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "number", + description = "the fee", + example = "100" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "number", description = "fee")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA}) + public String setRavencoinFeeCeiling(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String fee) { + Security.checkApiCallAllowed(request); + + Ravencoin ravencoin = Ravencoin.getInstance(); + + try { + return CrossChainUtils.setFeeCeiling(ravencoin, fee); + } + catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + } + } } diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index 1161dc63..d3919d9b 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -19,11 +19,14 @@ import org.qortal.api.model.CrossChainTradeSummary; import org.qortal.controller.tradebot.TradeBot; import org.qortal.crosschain.ACCT; import org.qortal.crosschain.AcctMode; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.ForeignBlockchainException; import org.qortal.crosschain.SupportedBlockchain; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.at.ATStateData; import org.qortal.data.crosschain.CrossChainTradeData; +import org.qortal.data.crosschain.TransactionSummary; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.MessageTransactionData; import org.qortal.data.transaction.TransactionData; @@ -47,6 +50,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import java.util.*; import java.util.function.Supplier; +import java.util.stream.Collectors; @Path("/crosschain") @Tag(name = "Cross-Chain") @@ -497,6 +501,111 @@ public class CrossChainResource { } } + @POST + @Path("/p2sh") + @Operation( + summary = "Returns P2SH Address", + description = "Get the P2SH address to lock foreign coin in a cross chain trade for QORT", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string", + description = "the AT address", + example = "AKFnu9yBp7tUAc5HAphhfCxRZTYoeKXgUy" + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "address")) + ) + } + ) + @ApiErrors({ApiError.ADDRESS_UNKNOWN, ApiError.INVALID_CRITERIA}) + @SecurityRequirement(name = "apiKey") + public String getForeignP2SH(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String atAddress) { + Security.checkApiCallAllowed(request); + + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = repository.getATRepository().fromATAddress(atAddress); + if (atData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash()); + + if( acct == null || !(acct.getBlockchain() instanceof Bitcoiny) ) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain(); + + CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData); + + Optional p2sh + = CrossChainUtils.getP2ShAddressForAT(atAddress, repository, bitcoiny, crossChainTradeData); + + if(p2sh.isPresent()){ + return p2sh.get(); + } + else{ + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + } + } + catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage()); + } + } + + @POST + @Path("/txactivity") + @Operation( + summary = "Returns Foreign Transaction Activity", + description = "Get the activity related to foreign coin trading", + responses = { + @ApiResponse( + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TransactionSummary.class + ) + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE}) + @SecurityRequirement(name = "apiKey") + public List getForeignTransactionActivity(@HeaderParam(Security.API_KEY_HEADER) String apiKey, @Parameter( + description = "Limit to specific blockchain", + example = "LITECOIN", + schema = @Schema(implementation = SupportedBlockchain.class) + ) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) { + Security.checkApiCallAllowed(request); + + if (!(foreignBlockchain.getInstance() instanceof Bitcoiny)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + Bitcoiny bitcoiny = (Bitcoiny) foreignBlockchain.getInstance() ; + + org.bitcoinj.core.Context.propagate( bitcoiny.getBitcoinjContext() ); + + try (final Repository repository = RepositoryManager.getRepository()) { + + // sort from last lock to first lock + return CrossChainUtils + .getForeignTradeSummaries(foreignBlockchain, repository, bitcoiny).stream() + .sorted(Comparator.comparing(TransactionSummary::getLockingTimestamp).reversed()) + .collect(Collectors.toList()); + } + catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage()); + } + catch (ForeignBlockchainException e) { + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage()); + } + } + private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException { ATData atData = repository.getATRepository().fromATAddress(atAddress); if (atData == null) diff --git a/src/main/java/org/qortal/api/resource/CrossChainUtils.java b/src/main/java/org/qortal/api/resource/CrossChainUtils.java new file mode 100644 index 00000000..b07a9d6c --- /dev/null +++ b/src/main/java/org/qortal/api/resource/CrossChainUtils.java @@ -0,0 +1,420 @@ +package org.qortal.api.resource; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; + +import org.qortal.crosschain.*; +import org.qortal.data.at.ATData; +import org.qortal.data.crosschain.AtomicTransactionData; +import org.qortal.data.crosschain.CrossChainTradeData; +import org.qortal.data.crosschain.TradeBotData; +import org.qortal.data.crosschain.TransactionSummary; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + + +public class CrossChainUtils { + private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class); + + public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) { + + BitcoinyBlockchainProvider blockchainProvider = blockchain.getBlockchainProvider(); + ChainableServer currentServer = blockchainProvider.getCurrentServer(); + + return new ServerConfigurationInfo( + buildInfos(blockchainProvider.getServers(), currentServer), + buildInfos(blockchainProvider.getRemainingServers(), currentServer), + buildInfos(blockchainProvider.getUselessServers(), currentServer) + ); + } + + public static ServerInfo buildInfo(ChainableServer server, boolean isCurrent) { + return new ServerInfo( + server.averageResponseTime(), + server.getHostName(), + server.getPort(), + server.getConnectionType().toString(), + isCurrent); + + } + + public static List buildInfos(Collection servers, ChainableServer currentServer) { + + List infos = new ArrayList<>( servers.size() ); + + for( ChainableServer server : servers ) + { + infos.add(buildInfo(server, server.equals(currentServer))); + } + + return infos; + } + + /** + * Set Fee Per Kb + * + * @param bitcoiny the blockchain support + * @param fee the fee in satoshis + * + * @return the fee if valid + * + * @throws IllegalArgumentException if invalid + */ + public static String setFeePerKb(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException { + + long satoshis = Long.parseLong(fee); + if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number"); + + bitcoiny.setFeePerKb(Coin.valueOf(satoshis) ); + + return String.valueOf(bitcoiny.getFeePerKb().value); + } + + /** + * Set Fee Ceiling + * + * @param bitcoiny the blockchain support + * @param fee the fee in satoshis + * + * @return the fee if valid + * + * @throws IllegalArgumentException if invalid + */ + public static String setFeeCeiling(Bitcoiny bitcoiny, String fee) throws IllegalArgumentException{ + + long satoshis = Long.parseLong(fee); + if( satoshis < 0 ) throw new IllegalArgumentException("can't set fee to negative number"); + + bitcoiny.setFeeCeiling( Long.parseLong(fee)); + + return String.valueOf(bitcoiny.getFeeCeiling()); + } + + /** + * Get P2Sh Address For AT + * + * @param atAddress the AT address + * @param repository the repository + * @param bitcoiny the blockchain data + * @param crossChainTradeData the trade data + * + * @return the p2sh address for the trade, if there is one + * + * @throws DataException + */ + public static Optional getP2ShAddressForAT( + String atAddress, + Repository repository, + Bitcoiny bitcoiny, + CrossChainTradeData crossChainTradeData) throws DataException { + + // get the trade bot data for the AT address + Optional tradeBotDataOptional + = repository.getCrossChainRepository() + .getAllTradeBotData().stream() + .filter(data -> data.getAtAddress().equals(atAddress)) + .findFirst(); + + if( tradeBotDataOptional.isEmpty() ) + return Optional.empty(); + + TradeBotData tradeBotData = tradeBotDataOptional.get(); + + // return the p2sh address from the trade bot + return getP2ShFromTradeBot(bitcoiny, crossChainTradeData, tradeBotData); + } + + /** + * Get Foreign Trade Summaries + * + * @param foreignBlockchain the blockchain traded on + * @param repository the repository + * @param bitcoiny data for the blockchain trade on + * @return + * @throws DataException + * @throws ForeignBlockchainException + */ + public static List getForeignTradeSummaries( + SupportedBlockchain foreignBlockchain, + Repository repository, + Bitcoiny bitcoiny) throws DataException, ForeignBlockchainException { + + // get all the AT address for the given blockchain + List atAddresses + = repository.getCrossChainRepository().getAllTradeBotData().stream() + .filter(data -> foreignBlockchain.name().toLowerCase().equals(data.getForeignBlockchain().toLowerCase())) + //.filter( data -> data.getForeignKey().equals( xpriv )) // TODO + .map(data -> data.getAtAddress()) + .collect(Collectors.toList()); + + List summaries = new ArrayList<>( atAddresses.size() * 2 ); + + // for each AT address, gather the data and get foreign trade summary + for( String atAddress: atAddresses) { + + ATData atData = repository.getATRepository().fromATAddress(atAddress); + + CrossChainTradeData crossChainTradeData = foreignBlockchain.getLatestAcct().populateTradeData(repository, atData); + + Optional address = getP2ShAddressForAT(atAddress,repository, bitcoiny, crossChainTradeData); + + if( address.isPresent()){ + summaries.add( getForeignTradeSummary( bitcoiny, address.get(), atAddress ) ); + } + } + + return summaries; + } + + /** + * Get P2Sh From Trade Bot + * + * Get P2Sh address from the trade bot + * + * @param bitcoiny the blockchain for the trade + * @param crossChainTradeData the cross cahin data for the trade + * @param tradeBotData the data from the trade bot + * + * @return the address, original format + */ + private static Optional getP2ShFromTradeBot( + Bitcoiny bitcoiny, + CrossChainTradeData crossChainTradeData, + TradeBotData tradeBotData) { + + // Pirate Chain does not support this + if( SupportedBlockchain.PIRATECHAIN.name().equals(tradeBotData.getForeignBlockchain())) return Optional.empty(); + + // need to get the trade PKH from the trade bot + if( tradeBotData.getTradeForeignPublicKeyHash() == null ) return Optional.empty(); + + // need to get the lock time from the trade bot + if( tradeBotData.getLockTimeA() == null ) return Optional.empty(); + + // need to get the creator PKH from the trade bot + if( crossChainTradeData.creatorForeignPKH == null ) return Optional.empty(); + + // need to get the secret from the trade bot + if( tradeBotData.getHashOfSecret() == null ) return Optional.empty(); + + // if we have the necessary data from the trade bot, + // then build the redeem script necessary to facilitate the trade + byte[] redeemScriptBytes + = BitcoinyHTLC.buildScript( + tradeBotData.getTradeForeignPublicKeyHash(), + tradeBotData.getLockTimeA(), + crossChainTradeData.creatorForeignPKH, + tradeBotData.getHashOfSecret() + ); + + + String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes); + + return Optional.of(p2shAddress); + } + + /** + * Get Foreign Trade Summary + * + * @param bitcoiny the blockchain the trade occurred on + * @param p2shAddress the p2sh address + * @param atAddress the AT address the p2sh address is derived from + * + * @return the summary + * + * @throws ForeignBlockchainException + */ + public static TransactionSummary getForeignTradeSummary(Bitcoiny bitcoiny, String p2shAddress, String atAddress) + throws ForeignBlockchainException { + Script outputScript = ScriptBuilder.createOutputScript( + Address.fromString(bitcoiny.getNetworkParameters(), p2shAddress)); + + List hashes + = bitcoiny.getAddressTransactions( outputScript.getProgram(), true); + + TransactionSummary summary; + + if(hashes.isEmpty()){ + summary + = new TransactionSummary( + atAddress, + p2shAddress, + "N/A", + "N/A", + 0, + 0, + 0, + 0, + "N/A", + 0, + 0, + 0, + 0); + } + else if( hashes.size() == 1) { + AtomicTransactionData data = buildTransactionData(bitcoiny, hashes.get(0)); + summary = new TransactionSummary( + atAddress, + p2shAddress, + "N/A", + data.hash.txHash, + data.timestamp, + data.totalAmount, + getTotalInput(bitcoiny, data.inputs) - data.totalAmount, + data.size, + "N/A", + 0, + 0, + 0, + 0); + } + // otherwise assuming there is 2 and only 2 hashes + else { + List atomicTransactionDataList = new ArrayList<>(2); + + // hashes -> data + for( TransactionHash hash : hashes){ + atomicTransactionDataList.add(buildTransactionData(bitcoiny,hash)); + } + + // sort the transaction data by time + List sorted + = atomicTransactionDataList.stream() + .sorted((data1, data2) -> data1.timestamp.compareTo(data2.timestamp)) + .collect(Collectors.toList()); + + // build the summary using the first 2 transactions + summary = buildForeignTradeSummary(atAddress, p2shAddress, sorted.get(0), sorted.get(1), bitcoiny); + } + return summary; + } + + /** + * Build Foreign Trade Summary + * + * @param p2shValue the p2sh address, original format + * @param lockingTransaction the transaction lock the foreighn coin + * @param unlockingTransaction the transaction to unlock the foreign coin + * @param bitcoiny the blockchain the trade occurred on + * + * @return + * + * @throws ForeignBlockchainException + */ + private static TransactionSummary buildForeignTradeSummary( + String atAddress, + String p2shValue, + AtomicTransactionData lockingTransaction, + AtomicTransactionData unlockingTransaction, + Bitcoiny bitcoiny) throws ForeignBlockchainException { + + // get sum of the relevant inputs for each transaction + long lockingTotalInput = getTotalInput(bitcoiny, lockingTransaction.inputs); + long unlockingTotalInput = getTotalInput(bitcoiny, unlockingTransaction.inputs); + + // find the address that has output that matches the total input + Optional, Long>> addressValue + = lockingTransaction.valueByAddress.entrySet().stream() + .filter(entry -> entry.getValue() == unlockingTotalInput).findFirst(); + + // set that matching address, if found + String p2shAddress; + if( addressValue.isPresent() && addressValue.get().getKey().size() == 1 ){ + p2shAddress = addressValue.get().getKey().get(0); + } + else { + p2shAddress = "N/A"; + } + + // build summaries with prepared values + // the fees are the total amount subtracted by the total transaction input + return new TransactionSummary( + atAddress, + p2shValue, + p2shAddress, + lockingTransaction.hash.txHash, + lockingTransaction.timestamp, + lockingTransaction.totalAmount, + lockingTotalInput - lockingTransaction.totalAmount, + lockingTransaction.size, + unlockingTransaction.hash.txHash, + unlockingTransaction.timestamp, + unlockingTransaction.totalAmount, + unlockingTotalInput - unlockingTransaction.totalAmount, + unlockingTransaction.size + ); + + } + + /** + * Build Transaction Data + * + * @param bitcoiny the coin for the transaction + * @param hash the hash for the transaction + * + * @return the data for the transaction + * + * @throws ForeignBlockchainException + */ + private static AtomicTransactionData buildTransactionData( Bitcoiny bitcoiny, TransactionHash hash) + throws ForeignBlockchainException { + + BitcoinyTransaction transaction = bitcoiny.getTransaction(hash.txHash); + + // destination address list -> value + Map, Long> valueByAddress = new HashMap<>(); + + // for each output in the transaction, index by address list + for( BitcoinyTransaction.Output output : transaction.outputs) { + valueByAddress.put(output.addresses, output.value); + } + + return new AtomicTransactionData( + hash, + transaction.timestamp, + transaction.inputs, + valueByAddress, + transaction.totalAmount, + transaction.size); + } + + /** + * Get Total Input + * + * Get the sum of all the inputs used in a list of inputs. + * + * @param bitcoiny the coin the inputs belong to + * @param inputs the inputs + * + * @return the sum + * + * @throws ForeignBlockchainException + */ + private static long getTotalInput(Bitcoiny bitcoiny, List inputs) + throws ForeignBlockchainException { + + long totalInputOut = 0; + + // for each input, add to total input, + // get the indexed transaction output value and add to total value + for( BitcoinyTransaction.Input input : inputs){ + + BitcoinyTransaction inputOut = bitcoiny.getTransaction(input.outputTxHash); + BitcoinyTransaction.Output output = inputOut.outputs.get(input.outputVout); + totalInputOut += output.value; + } + return totalInputOut; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/PollsResource.java b/src/main/java/org/qortal/api/resource/PollsResource.java index aaa4f79c..8b00d3cd 100644 --- a/src/main/java/org/qortal/api/resource/PollsResource.java +++ b/src/main/java/org/qortal/api/resource/PollsResource.java @@ -13,6 +13,8 @@ import org.qortal.api.ApiErrors; import org.qortal.api.ApiException; import org.qortal.api.ApiExceptionFactory; import org.qortal.api.model.PollVotes; +import org.qortal.crypto.Crypto; +import org.qortal.data.account.AccountData; import org.qortal.data.transaction.CreatePollTransactionData; import org.qortal.data.transaction.VoteOnPollTransactionData; import org.qortal.data.voting.PollData; @@ -129,12 +131,25 @@ public class PollsResource { for (PollOptionData optionData : pollData.getPollOptions()) { voteCountMap.put(optionData.getOptionName(), 0); } + // Initialize map for counting vote weights + Map voteWeightMap = new HashMap<>(); + for (PollOptionData optionData : pollData.getPollOptions()) { + voteWeightMap.put(optionData.getOptionName(), 0); + } int totalVotes = 0; + int totalWeight = 0; for (VoteOnPollData vote : votes) { + String voter = Crypto.toAddress(vote.getVoterPublicKey()); + AccountData voterData = repository.getAccountRepository().getAccount(voter); + int voteWeight = voterData.getBlocksMinted() + voterData.getBlocksMintedPenalty(); + if (voteWeight < 0) voteWeight = 0; + totalWeight += voteWeight; + String selectedOption = pollData.getPollOptions().get(vote.getOptionIndex()).getOptionName(); if (voteCountMap.containsKey(selectedOption)) { voteCountMap.put(selectedOption, voteCountMap.get(selectedOption) + 1); + voteWeightMap.put(selectedOption, voteWeightMap.get(selectedOption) + voteWeight); totalVotes++; } } @@ -143,11 +158,15 @@ public class PollsResource { List voteCounts = voteCountMap.entrySet().stream() .map(entry -> new PollVotes.OptionCount(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); + // Convert map to list of WeightInfo + List voteWeights = voteWeightMap.entrySet().stream() + .map(entry -> new PollVotes.OptionWeight(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); if (onlyCounts != null && onlyCounts) { - return new PollVotes(null, totalVotes, voteCounts); + return new PollVotes(null, totalVotes, totalWeight, voteCounts, voteWeights); } else { - return new PollVotes(votes, totalVotes, voteCounts); + return new PollVotes(votes, totalVotes, totalWeight, voteCounts, voteWeights); } } catch (ApiException e) { throw e; diff --git a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java index f0e045ed..837288e5 100644 --- a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java @@ -24,8 +24,9 @@ import org.qortal.api.model.ActivitySummary; import org.qortal.api.model.NodeInfo; import org.qortal.api.model.NodeStatus; import org.qortal.block.BlockChain; -import org.qortal.controller.AutoUpdate; +import org.qortal.controller.BootstrapNode; import org.qortal.controller.Controller; +import org.qortal.controller.RestartNode; import org.qortal.controller.Synchronizer; import org.qortal.controller.Synchronizer.SynchronizationResult; import org.qortal.controller.repository.BlockArchiveRebuilder; @@ -250,7 +251,38 @@ public class AdminResource { // Not important } - AutoUpdate.attemptRestart(); + RestartNode.attemptToRestart(); + + }).start(); + + return "true"; + } + + @GET + @Path("/bootstrap") + @Operation( + summary = "Bootstrap", + description = "Delete and download new database archive", + responses = { + @ApiResponse( + description = "\"true\"", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @SecurityRequirement(name = "apiKey") + public String bootstrap(@HeaderParam(Security.API_KEY_HEADER) String apiKey) { + Security.checkApiCallAllowed(request); + + new Thread(() -> { + // Short sleep to allow HTTP response body to be emitted + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Not important + } + + BootstrapNode.attemptToBootstrap(); }).start(); diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index 93dac568..276116fc 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -522,6 +522,10 @@ public class QortalATAPI extends API { /** Returns AT account's lastReference */ private byte[] getLastReference() { + // If we have transactions already, then use signature from last transaction + if (!this.transactions.isEmpty()) + return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature(); + try { // Look up AT's account's last reference from repository Account atAccount = this.getATAccount(); diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 41faf51b..caaa0c76 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -1061,8 +1061,10 @@ public class Block { return ValidationResult.MINTER_NOT_ACCEPTED; long expectedTimestamp = calcTimestamp(parentBlockData, this.blockData.getMinterPublicKey(), minterLevel); - if (this.blockData.getTimestamp() != expectedTimestamp) + if (this.blockData.getTimestamp() != expectedTimestamp) { + LOGGER.debug(String.format("timestamp mismatch! block had %s but we expected %s", this.blockData.getTimestamp(), expectedTimestamp)); return ValidationResult.TIMESTAMP_INCORRECT; + } return ValidationResult.OK; } @@ -1309,6 +1311,9 @@ public class Block { if (!transaction.isConfirmable()) { return ValidationResult.TRANSACTION_NOT_CONFIRMABLE; } + if (!transaction.isConfirmableAtHeight(this.blockData.getHeight())) { + return ValidationResult.TRANSACTION_NOT_CONFIRMABLE; + } } // Check transaction isn't already included in a block @@ -1545,12 +1550,22 @@ public class Block { processBlockRewards(); } - if (this.blockData.getHeight() == 212937) + if (this.blockData.getHeight() == 212937) { // Apply fix for block 212937 Block212937.processFix(this); + } - else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) + if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) { SelfSponsorshipAlgoV1Block.processAccountPenalties(this); + } + + if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV2Height()) { + SelfSponsorshipAlgoV2Block.processAccountPenalties(this); + } + + if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) { + SelfSponsorshipAlgoV3Block.processAccountPenalties(this); + } } // We're about to (test-)process a batch of transactions, @@ -1835,13 +1850,23 @@ public class Block { // Invalidate expandedAccounts as they may have changed due to orphaning TRANSFER_PRIVS transactions, etc. this.cachedExpandedAccounts = null; - if (this.blockData.getHeight() == 212937) + if (this.blockData.getHeight() == 212937) { // Revert fix for block 212937 Block212937.orphanFix(this); + } - else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) + if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height()) { SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this); + } + if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV2Height()) { + SelfSponsorshipAlgoV2Block.orphanAccountPenalties(this); + } + + if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV3Height()) { + SelfSponsorshipAlgoV3Block.orphanAccountPenalties(this); + } + // Account levels and block rewards are only processed/orphaned on block reward distribution blocks if (this.isRewardDistributionBlock()) { // Block rewards, including transaction fees, removed after transactions undone @@ -2088,7 +2113,7 @@ public class Block { return Block.isOnlineAccountsBlock(this.getBlockData().getHeight()); } - private static boolean isOnlineAccountsBlock(int height) { + public static boolean isOnlineAccountsBlock(int height) { // After feature trigger, only certain blocks contain online accounts if (height >= BlockChain.getInstance().getBlockRewardBatchStartHeight()) { final int leadingBlockCount = BlockChain.getInstance().getBlockRewardBatchAccountsBlockCount(); @@ -2539,5 +2564,4 @@ public class Block { LOGGER.info(() -> String.format("Unable to log block debugging info: %s", e.getMessage())); } } - } diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index aa2ab9bb..dc9dfe4c 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -73,9 +73,14 @@ public class BlockChain { increaseOnlineAccountsDifficultyTimestamp, onlineAccountMinterLevelValidationHeight, selfSponsorshipAlgoV1Height, + selfSponsorshipAlgoV2Height, + selfSponsorshipAlgoV3Height, feeValidationFixTimestamp, chatReferenceTimestamp, - arbitraryOptionalFeeTimestamp; + arbitraryOptionalFeeTimestamp, + unconfirmableRewardSharesHeight, + disableTransferPrivsTimestamp, + enableTransferPrivsTimestamp } // Custom transaction fees @@ -198,6 +203,7 @@ public class BlockChain { /** Minimum time to retain online account signatures (ms) for block validity checks. */ private long onlineAccountSignaturesMinLifetime; + /** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */ private long onlineAccountSignaturesMaxLifetime; @@ -208,6 +214,15 @@ public class BlockChain { /** Snapshot timestamp for self sponsorship algo V1 */ private long selfSponsorshipAlgoV1SnapshotTimestamp; + /** Snapshot timestamp for self sponsorship algo V2 */ + private long selfSponsorshipAlgoV2SnapshotTimestamp; + + /** Snapshot timestamp for self sponsorship algo V3 */ + private long selfSponsorshipAlgoV3SnapshotTimestamp; + + /** Reference timestamp for self sponsorship algo V1 block height */ + private long referenceTimestampBlock; + /** Feature-trigger timestamp to modify behaviour of various transactions that support mempow */ private long mempowTransactionUpdatesTimestamp; @@ -224,6 +239,8 @@ public class BlockChain { * data and to base online accounts decisions on. */ private int blockRewardBatchAccountsBlockCount; + private String penaltyFixHash; + /** Max reward shares by block height */ public static class MaxRewardSharesByTimestamp { public long timestamp; @@ -266,7 +283,7 @@ public class BlockChain { try { // Create JAXB context aware of Settings jc = JAXBContextFactory.createContext(new Class[] { - BlockChain.class, GenesisBlock.GenesisInfo.class + BlockChain.class, GenesisBlock.GenesisInfo.class }, null); // Create unmarshaller @@ -394,12 +411,29 @@ public class BlockChain { return this.blockRewardBatchAccountsBlockCount; } + public String getPenaltyFixHash() { + return this.penaltyFixHash; + } - // Self sponsorship algo + // Self sponsorship algo V1 public long getSelfSponsorshipAlgoV1SnapshotTimestamp() { return this.selfSponsorshipAlgoV1SnapshotTimestamp; } + // Self sponsorship algo V2 + public long getSelfSponsorshipAlgoV2SnapshotTimestamp() { + return this.selfSponsorshipAlgoV2SnapshotTimestamp; + } + + // Self sponsorship algo V3 + public long getSelfSponsorshipAlgoV3SnapshotTimestamp() { + return this.selfSponsorshipAlgoV3SnapshotTimestamp; + } + + // Self sponsorship algo V3 + public long getReferenceTimestampBlock() { + return this.referenceTimestampBlock; + } // Feature-trigger timestamp to modify behaviour of various transactions that support mempow public long getMemPoWTransactionUpdatesTimestamp() { return this.mempowTransactionUpdatesTimestamp; @@ -540,6 +574,14 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV1Height.name()).intValue(); } + public int getSelfSponsorshipAlgoV2Height() { + return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV2Height.name()).intValue(); + } + + public int getSelfSponsorshipAlgoV3Height() { + return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV3Height.name()).intValue(); + } + public long getOnlineAccountMinterLevelValidationHeight() { return this.featureTriggers.get(FeatureTrigger.onlineAccountMinterLevelValidationHeight.name()).intValue(); } @@ -556,6 +598,17 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.arbitraryOptionalFeeTimestamp.name()).longValue(); } + public int getUnconfirmableRewardSharesHeight() { + return this.featureTriggers.get(FeatureTrigger.unconfirmableRewardSharesHeight.name()).intValue(); + } + + public long getDisableTransferPrivsTimestamp() { + return this.featureTriggers.get(FeatureTrigger.disableTransferPrivsTimestamp.name()).longValue(); + } + + public long getEnableTransferPrivsTimestamp() { + return this.featureTriggers.get(FeatureTrigger.enableTransferPrivsTimestamp.name()).longValue(); + } // More complex getters for aspects that change by height or timestamp @@ -742,7 +795,7 @@ public class BlockChain { /** * Some sort of start-up/initialization/checking method. - * + * * @throws SQLException */ public static void validate() throws DataException { diff --git a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java index c3c374d1..27b50a81 100644 --- a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java +++ b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java @@ -28,7 +28,6 @@ public final class SelfSponsorshipAlgoV1Block { private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV1Block.class); - private SelfSponsorshipAlgoV1Block() { /* Do not instantiate */ } @@ -133,4 +132,4 @@ public final class SelfSponsorshipAlgoV1Block { return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8))); } -} +} \ No newline at end of file diff --git a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java new file mode 100644 index 00000000..7957de6a --- /dev/null +++ b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV2Block.java @@ -0,0 +1,143 @@ +package org.qortal.block; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.account.SelfSponsorshipAlgoV2; +import org.qortal.api.model.AccountPenaltyStats; +import org.qortal.crypto.Crypto; +import org.qortal.data.account.AccountData; +import org.qortal.data.account.AccountPenaltyData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.utils.Base58; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Self Sponsorship AlgoV2 Block + *

+ * Selected block for the initial run on the "self sponsorship detection algorithm" + */ +public final class SelfSponsorshipAlgoV2Block { + + private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV2Block.class); + + private SelfSponsorshipAlgoV2Block() { + /* Do not instantiate */ + } + + public static void processAccountPenalties(Block block) throws DataException { + LOGGER.info("Process Self Sponsorship Algo V2 - this will take a while..."); + logPenaltyStats(block.repository); + long startTime = System.currentTimeMillis(); + Set penalties = getAccountPenalties(block.repository, -5000000); + block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties); + long totalTime = System.currentTimeMillis() - startTime; + String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList())); + LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f)); + logPenaltyStats(block.repository); + + int updatedCount = updateAccountLevels(block.repository, penalties); + LOGGER.info("Account levels updated for {} penalty addresses", updatedCount); + } + + public static void orphanAccountPenalties(Block block) throws DataException { + LOGGER.info("Orphan Self Sponsorship Algo V2 - this will take a while..."); + logPenaltyStats(block.repository); + long startTime = System.currentTimeMillis(); + Set penalties = getAccountPenalties(block.repository, 5000000); + block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties); + long totalTime = System.currentTimeMillis() - startTime; + String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList())); + LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f)); + logPenaltyStats(block.repository); + + int updatedCount = updateAccountLevels(block.repository, penalties); + LOGGER.info("Account levels updated for {} penalty addresses", updatedCount); + } + + private static Set getAccountPenalties(Repository repository, int penalty) throws DataException { + Set penalties = new LinkedHashSet<>(); + List penalizedAddresses = repository.getAccountRepository().getPenaltyAccounts(); + List assetAddresses = repository.getTransactionRepository().getConfirmedTransferAssetCreators(); + + for (AccountData penalizedAddress : penalizedAddresses) { + //System.out.println(String.format("address: %s", address)); + SelfSponsorshipAlgoV2 selfSponsorshipAlgoV2 = new SelfSponsorshipAlgoV2(repository, penalizedAddress.getAddress(), false); + selfSponsorshipAlgoV2.run(); + //System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV2.getPenaltyAddresses().size())); + for (String penaltyAddress : selfSponsorshipAlgoV2.getPenaltyAddresses()) { + penalties.add(new AccountPenaltyData(penaltyAddress, penalty)); + } + } + + for (String assetAddress : assetAddresses) { + //System.out.println(String.format("address: %s", address)); + SelfSponsorshipAlgoV2 selfSponsorshipAlgoV2 = new SelfSponsorshipAlgoV2(repository, assetAddress, true); + selfSponsorshipAlgoV2.run(); + //System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV2.getPenaltyAddresses().size())); + for (String penaltyAddress : selfSponsorshipAlgoV2.getPenaltyAddresses()) { + penalties.add(new AccountPenaltyData(penaltyAddress, penalty)); + } + } + + return penalties; + } + + private static int updateAccountLevels(Repository repository, Set accountPenalties) throws DataException { + final List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + final int maximumLevel = cumulativeBlocksByLevel.size() - 1; + + int updatedCount = 0; + + for (AccountPenaltyData penaltyData : accountPenalties) { + AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress()); + final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty(); + + // Shortcut for penalties + if (effectiveBlocksMinted < 0) { + accountData.setLevel(0); + repository.getAccountRepository().setLevel(accountData); + updatedCount++; + LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel())); + continue; + } + + for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) { + if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) { + accountData.setLevel(newLevel); + repository.getAccountRepository().setLevel(accountData); + updatedCount++; + LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel())); + break; + } + } + } + + return updatedCount; + } + + private static void logPenaltyStats(Repository repository) { + try { + LOGGER.info(getPenaltyStats(repository)); + + } catch (DataException e) {} + } + + private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException { + List accounts = repository.getAccountRepository().getPenaltyAccounts(); + return AccountPenaltyStats.fromAccounts(accounts); + } + + public static String getHash(List penaltyAddresses) { + if (penaltyAddresses == null || penaltyAddresses.isEmpty()) { + return null; + } + Collections.sort(penaltyAddresses); + return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8))); + } + +} \ No newline at end of file diff --git a/src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java new file mode 100644 index 00000000..dec8957d --- /dev/null +++ b/src/main/java/org/qortal/block/SelfSponsorshipAlgoV3Block.java @@ -0,0 +1,136 @@ +package org.qortal.block; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.account.SelfSponsorshipAlgoV3; +import org.qortal.api.model.AccountPenaltyStats; +import org.qortal.crypto.Crypto; +import org.qortal.data.account.AccountData; +import org.qortal.data.account.AccountPenaltyData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.utils.Base58; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Self Sponsorship AlgoV3 Block + *

+ * Selected block for the initial run on the "self sponsorship detection algorithm" + */ +public final class SelfSponsorshipAlgoV3Block { + + private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV3Block.class); + + private SelfSponsorshipAlgoV3Block() { + /* Do not instantiate */ + } + + public static void processAccountPenalties(Block block) throws DataException { + LOGGER.info("Process Self Sponsorship Algo V3 - this will take a while..."); + logPenaltyStats(block.repository); + long startTime = System.currentTimeMillis(); + Set penalties = getAccountPenalties(block.repository, -5000000); + block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties); + long totalTime = System.currentTimeMillis() - startTime; + String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList())); + LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f)); + logPenaltyStats(block.repository); + + int updatedCount = updateAccountLevels(block.repository, penalties); + LOGGER.info("Account levels updated for {} penalty addresses", updatedCount); + } + + public static void orphanAccountPenalties(Block block) throws DataException { + LOGGER.info("Orphan Self Sponsorship Algo V3 - this will take a while..."); + logPenaltyStats(block.repository); + long startTime = System.currentTimeMillis(); + Set penalties = getAccountPenalties(block.repository, 5000000); + block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties); + long totalTime = System.currentTimeMillis() - startTime; + String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList())); + LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f)); + logPenaltyStats(block.repository); + + int updatedCount = updateAccountLevels(block.repository, penalties); + LOGGER.info("Account levels updated for {} penalty addresses", updatedCount); + } + + public static Set getAccountPenalties(Repository repository, int penalty) throws DataException { + final long snapshotTimestampV1 = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp(); + final long snapshotTimestampV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3SnapshotTimestamp(); + Set penalties = new LinkedHashSet<>(); + List addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares(); + for (String address : addresses) { + //System.out.println(String.format("address: %s", address)); + SelfSponsorshipAlgoV3 selfSponsorshipAlgoV3 = new SelfSponsorshipAlgoV3(repository, address, snapshotTimestampV1, snapshotTimestampV3, false); + selfSponsorshipAlgoV3.run(); + //System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV3.getPenaltyAddresses().size())); + + for (String penaltyAddress : selfSponsorshipAlgoV3.getPenaltyAddresses()) { + penalties.add(new AccountPenaltyData(penaltyAddress, penalty)); + } + } + return penalties; + } + + private static int updateAccountLevels(Repository repository, Set accountPenalties) throws DataException { + final List cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel(); + final int maximumLevel = cumulativeBlocksByLevel.size() - 1; + + int updatedCount = 0; + + for (AccountPenaltyData penaltyData : accountPenalties) { + AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress()); + final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty(); + + // Shortcut for penalties + if (effectiveBlocksMinted < 0) { + accountData.setLevel(0); + repository.getAccountRepository().setLevel(accountData); + updatedCount++; + LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel())); + continue; + } + + for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) { + if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) { + accountData.setLevel(newLevel); + repository.getAccountRepository().setLevel(accountData); + updatedCount++; + LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel())); + break; + } + } + } + + return updatedCount; + } + + private static void logPenaltyStats(Repository repository) { + try { + LOGGER.info(getPenaltyStats(repository)); + + } catch (DataException e) {} + } + + private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException { + List accounts = repository.getAccountRepository().getPenaltyAccounts(); + return AccountPenaltyStats.fromAccounts(accounts); + } + + public static String getHash(List penaltyAddresses) { + if (penaltyAddresses == null || penaltyAddresses.isEmpty()) { + return null; + } + Collections.sort(penaltyAddresses); + return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8))); + } + +} \ No newline at end of file diff --git a/src/main/java/org/qortal/controller/AutoUpdate.java b/src/main/java/org/qortal/controller/AutoUpdate.java index bc232e1b..4b315651 100644 --- a/src/main/java/org/qortal/controller/AutoUpdate.java +++ b/src/main/java/org/qortal/controller/AutoUpdate.java @@ -291,78 +291,4 @@ public class AutoUpdate extends Thread { return true; // repo was okay, even if applying update failed } } - - public static boolean attemptRestart() { - LOGGER.info(String.format("Restarting node...")); - - // Give repository a chance to backup in case things go badly wrong (if enabled) - if (Settings.getInstance().getRepositoryBackupInterval() > 0) { - try { - // Timeout if the database isn't ready for backing up after 60 seconds - long timeout = 60 * 1000L; - RepositoryManager.backup(true, "backup", timeout); - - } catch (TimeoutException e) { - LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage()); - // Continue with the node restart anyway... - } - } - - // Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced) - String javaHome = System.getProperty("java.home"); - LOGGER.debug(String.format("Java home: %s", javaHome)); - - Path javaBinary = Paths.get(javaHome, "bin", "java"); - LOGGER.debug(String.format("Java binary: %s", javaBinary)); - - try { - List javaCmd = new ArrayList<>(); - // Java runtime binary itself - javaCmd.add(javaBinary.toString()); - - // JVM arguments - javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); - - // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port - javaCmd = javaCmd.stream() - .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG)) - .collect(Collectors.toList()); - - // Remove JNI options as they won't be supported by command-line 'java' - // These are typically added by the AdvancedInstaller Java launcher EXE - javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf")); - - // Call ApplyUpdate using JAR - javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyUpdate.class.getCanonicalName())); - - // Add command-line args saved from start-up - String[] savedArgs = Controller.getInstance().getSavedArgs(); - if (savedArgs != null) - javaCmd.addAll(Arrays.asList(savedArgs)); - - LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); - - SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"), //TODO - Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"), //TODO - MessageType.INFO); - - ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); - - // New process will inherit our stdout and stderr - processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); - processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); - - Process process = processBuilder.start(); - - // Nothing to pipe to new process, so close output stream (process's stdin) - process.getOutputStream().close(); - - return true; // restarting node OK - } catch (Exception e) { - LOGGER.error(String.format("Failed to restart node: %s", e.getMessage())); - - return true; // repo was okay, even if applying update failed - } - } - } diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java index 15bcb1d7..49831cba 100644 --- a/src/main/java/org/qortal/controller/BlockMinter.java +++ b/src/main/java/org/qortal/controller/BlockMinter.java @@ -474,6 +474,7 @@ public class BlockMinter extends Thread { Iterator unconfirmedTransactionsIterator = unconfirmedTransactions.iterator(); final long newBlockTimestamp = newBlock.getBlockData().getTimestamp(); + final int newBlockHeight = newBlock.getBlockData().getHeight(); while (unconfirmedTransactionsIterator.hasNext()) { TransactionData transactionData = unconfirmedTransactionsIterator.next(); @@ -481,6 +482,12 @@ public class BlockMinter extends Thread { // Ignore transactions that have expired before this block - they will be cleaned up later if (transactionData.getTimestamp() > newBlockTimestamp || Transaction.getDeadline(transactionData) <= newBlockTimestamp) unconfirmedTransactionsIterator.remove(); + + // Ignore transactions that are unconfirmable at this block height + Transaction transaction = Transaction.fromData(repository, transactionData); + if (!transaction.isConfirmableAtHeight(newBlockHeight)) { + unconfirmedTransactionsIterator.remove(); + } } // Sign to create block's signature, needed by Block.isValid() diff --git a/src/main/java/org/qortal/controller/BootstrapNode.java b/src/main/java/org/qortal/controller/BootstrapNode.java new file mode 100644 index 00000000..9d0f8b36 --- /dev/null +++ b/src/main/java/org/qortal/controller/BootstrapNode.java @@ -0,0 +1,103 @@ +package org.qortal.controller; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.ApplyBootstrap; +import org.qortal.globalization.Translator; +import org.qortal.gui.SysTray; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; + +import java.awt.TrayIcon.MessageType; +import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +/* NOTE: It is CRITICAL that we use OpenJDK and not Java SE because our uber jar repacks BouncyCastle which, in turn, unsigns BC causing it to be rejected as a security provider by Java SE. */ + +public class BootstrapNode { + + public static final String JAR_FILENAME = "qortal.jar"; + public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib="; + + private static final Logger LOGGER = LogManager.getLogger(BootstrapNode.class); + + public static boolean attemptToBootstrap() { + LOGGER.info(String.format("Bootstrapping node...")); + + // Give repository a chance to backup in case things go badly wrong (if enabled) + if (Settings.getInstance().getRepositoryBackupInterval() > 0) { + try { + // Timeout if the database isn't ready for backing up after 60 seconds + long timeout = 60 * 1000L; + RepositoryManager.backup(true, "backup", timeout); + + } catch (TimeoutException e) { + LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage()); + // Continue with the bootstrap anyway... + } + } + + // Call ApplyBootstrap to end this process + String javaHome = System.getProperty("java.home"); + LOGGER.debug(String.format("Java home: %s", javaHome)); + + Path javaBinary = Paths.get(javaHome, "bin", "java"); + LOGGER.debug(String.format("Java binary: %s", javaBinary)); + + try { + List javaCmd = new ArrayList<>(); + + // Java runtime binary itself + javaCmd.add(javaBinary.toString()); + + // JVM arguments + javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + + // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port + javaCmd = javaCmd.stream() + .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG)) + .collect(Collectors.toList()); + + // Remove JNI options as they won't be supported by command-line 'java' + // These are typically added by the AdvancedInstaller Java launcher EXE + javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf")); + + // Call ApplyBootstrap using JAR + javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyBootstrap.class.getCanonicalName())); + + // Add command-line args saved from start-up + String[] savedArgs = Controller.getInstance().getSavedArgs(); + if (savedArgs != null) + javaCmd.addAll(Arrays.asList(savedArgs)); + + LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + + SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "BOOTSTRAP_NODE"), + Translator.INSTANCE.translate("SysTray", "APPLYING_BOOTSTRAP_AND_RESTARTING"), + MessageType.INFO); + + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + // New process will inherit our stdout and stderr + processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = processBuilder.start(); + + // Nothing to pipe to new process, so close output stream (process's stdin) + process.getOutputStream().close(); + + return true; // restarting node OK + } catch (Exception e) { + LOGGER.error(String.format("Failed to restart node: %s", e.getMessage())); + + return true; // repo was okay, even if applying bootstrap failed + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 6d2562ab..c1e0e279 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -31,6 +31,7 @@ import org.qortal.globalization.Translator; import org.qortal.gui.Gui; import org.qortal.gui.SysTray; import org.qortal.network.Network; +import org.qortal.network.RNSNetwork; import org.qortal.network.Peer; import org.qortal.network.message.*; import org.qortal.repository.*; @@ -115,6 +116,7 @@ public class Controller extends Thread { private long repositoryCheckpointTimestamp = startTime; // ms private long prunePeersTimestamp = startTime; // ms private long ntpCheckTimestamp = startTime; // ms + private long pruneRNSPeersTimestamp = startTime; // ms private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms /** Whether we can mint new blocks, as reported by BlockMinter. */ @@ -481,6 +483,15 @@ public class Controller extends Thread { return; // Not System.exit() so that GUI can display error } + LOGGER.info("Starting Reticulum"); + try { + RNSNetwork rns = RNSNetwork.getInstance(); + rns.start(); + LOGGER.debug("Reticulum instance: {}", rns.toString()); + } catch (IOException | DataException e) { + LOGGER.error("Unable to start Reticulum", e); + } + Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { @@ -582,6 +593,8 @@ public class Controller extends Thread { final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval(); long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval(); final long prunePeersInterval = 5 * 60 * 1000L; // Every 5 minutes + //final long pruneRNSPeersInterval = 5 * 60 * 1000L; // Every 5 minutes + final long pruneRNSPeersInterval = 1 * 60 * 1000L; // Every 1 minute (during development) // Start executor service for trimming or pruning PruneManager.getInstance().start(); @@ -690,6 +703,18 @@ public class Controller extends Thread { } } + // Q: Do we need global pruning? + if (now >= pruneRNSPeersTimestamp + pruneRNSPeersInterval) { + pruneRNSPeersTimestamp = now + pruneRNSPeersInterval; + + try { + LOGGER.debug("Pruning Reticulum peers..."); + RNSNetwork.getInstance().prunePeers(); + } catch (DataException e) { + LOGGER.warn(String.format("Repository issue when trying to prune Reticulum peers: %s", e.getMessage())); + } + } + // Delete expired transactions if (now >= deleteExpiredTimestamp) { deleteExpiredTimestamp = now + DELETE_EXPIRED_INTERVAL; @@ -988,6 +1013,9 @@ public class Controller extends Thread { LOGGER.info("Shutting down networking"); Network.getInstance().shutdown(); + LOGGER.info("Shutting down Reticulum"); + RNSNetwork.getInstance().shutdown(); + LOGGER.info("Shutting down controller"); this.interrupt(); try { diff --git a/src/main/java/org/qortal/controller/RestartNode.java b/src/main/java/org/qortal/controller/RestartNode.java new file mode 100644 index 00000000..b8d81b85 --- /dev/null +++ b/src/main/java/org/qortal/controller/RestartNode.java @@ -0,0 +1,102 @@ +package org.qortal.controller; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.ApplyRestart;; +import org.qortal.globalization.Translator; +import org.qortal.gui.SysTray; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; + +import java.awt.TrayIcon.MessageType; +import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +/* NOTE: It is CRITICAL that we use OpenJDK and not Java SE because our uber jar repacks BouncyCastle which, in turn, unsigns BC causing it to be rejected as a security provider by Java SE. */ + +public class RestartNode { + + public static final String JAR_FILENAME = "qortal.jar"; + public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib="; + + private static final Logger LOGGER = LogManager.getLogger(RestartNode.class); + + public static boolean attemptToRestart() { + LOGGER.info(String.format("Restarting node...")); + + // Give repository a chance to backup in case things go badly wrong (if enabled) + if (Settings.getInstance().getRepositoryBackupInterval() > 0) { + try { + // Timeout if the database isn't ready for backing up after 60 seconds + long timeout = 60 * 1000L; + RepositoryManager.backup(true, "backup", timeout); + + } catch (TimeoutException e) { + LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage()); + // Continue with the node restart anyway... + } + } + + // Call ApplyRestart to end this process + String javaHome = System.getProperty("java.home"); + LOGGER.debug(String.format("Java home: %s", javaHome)); + + Path javaBinary = Paths.get(javaHome, "bin", "java"); + LOGGER.debug(String.format("Java binary: %s", javaBinary)); + + try { + List javaCmd = new ArrayList<>(); + // Java runtime binary itself + javaCmd.add(javaBinary.toString()); + + // JVM arguments + javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + + // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port + javaCmd = javaCmd.stream() + .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG)) + .collect(Collectors.toList()); + + // Remove JNI options as they won't be supported by command-line 'java' + // These are typically added by the AdvancedInstaller Java launcher EXE + javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf")); + + // Call ApplyRestart using JAR + javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyRestart.class.getCanonicalName())); + + // Add command-line args saved from start-up + String[] savedArgs = Controller.getInstance().getSavedArgs(); + if (savedArgs != null) + javaCmd.addAll(Arrays.asList(savedArgs)); + + LOGGER.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + + SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "RESTARTING_NODE"), + Translator.INSTANCE.translate("SysTray", "APPLYING_RESTARTING_NODE"), + MessageType.INFO); + + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + // New process will inherit our stdout and stderr + processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = processBuilder.start(); + + // Nothing to pipe to new process, so close output stream (process's stdin) + process.getOutputStream().close(); + + return true; // restarting node OK + } catch (Exception e) { + LOGGER.error(String.format("Failed to restart node: %s", e.getMessage())); + + return true; // repo was okay, even if applying restart failed + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/Bitcoin.java b/src/main/java/org/qortal/crosschain/Bitcoin.java index 801b141c..79ab38af 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoin.java +++ b/src/main/java/org/qortal/crosschain/Bitcoin.java @@ -7,7 +7,7 @@ import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; import org.qortal.crosschain.ElectrumX.Server; -import org.qortal.crosschain.ElectrumX.Server.ConnectionType; +import org.qortal.crosschain.ChainableServer.ConnectionType; import org.qortal.settings.Settings; import java.util.Arrays; @@ -22,8 +22,6 @@ public class Bitcoin extends Bitcoiny { private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees // Temporary values until a dynamic fee system is written. - private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees. - private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch private static final long NEW_FEE_AMOUNT = 6_000L; private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST @@ -46,74 +44,62 @@ public class Bitcoin extends Bitcoiny { return Arrays.asList( // Servers chosen on NO BASIS WHATSOEVER from various sources! // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=btc - new Server("104.248.139.211", Server.ConnectionType.SSL, 50002), + new Server("104.198.149.61", Server.ConnectionType.SSL, 50002), new Server("128.0.190.26", Server.ConnectionType.SSL, 50002), - new Server("142.93.6.38", Server.ConnectionType.SSL, 50002), new Server("157.245.172.236", Server.ConnectionType.SSL, 50002), - new Server("167.172.226.175", Server.ConnectionType.SSL, 50002), - new Server("167.172.42.31", Server.ConnectionType.SSL, 50002), - new Server("178.62.80.20", Server.ConnectionType.SSL, 50002), - new Server("185.64.116.15", Server.ConnectionType.SSL, 50002), - new Server("188.165.206.215", Server.ConnectionType.SSL, 50002), - new Server("188.165.211.112", Server.ConnectionType.SSL, 50002), - new Server("2azzarita.hopto.org", Server.ConnectionType.SSL, 50002), - new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022), - new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002), - new Server("65.39.140.37", Server.ConnectionType.SSL, 50002), - new Server("68.183.188.105", Server.ConnectionType.SSL, 50002), - new Server("71.73.14.254", Server.ConnectionType.SSL, 50002), - new Server("94.23.247.135", Server.ConnectionType.SSL, 50002), - new Server("assuredly.not.fyi", Server.ConnectionType.SSL, 50002), - new Server("ax101.blockeng.ch", Server.ConnectionType.SSL, 50002), - new Server("ax102.blockeng.ch", Server.ConnectionType.SSL, 50002), + new Server("260.whyza.net", Server.ConnectionType.SSL, 50002), + new Server("34.136.93.37", Server.ConnectionType.SSL, 50002), + new Server("34.67.22.216", Server.ConnectionType.SSL, 50002), + new Server("34.68.133.78", Server.ConnectionType.SSL, 50002), + new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002), new Server("b.1209k.com", Server.ConnectionType.SSL, 50002), new Server("b6.1209k.com", Server.ConnectionType.SSL, 50002), new Server("bitcoin.dermichi.com", Server.ConnectionType.SSL, 50002), new Server("bitcoin.lu.ke", Server.ConnectionType.SSL, 50002), new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002), new Server("blkhub.net", Server.ConnectionType.SSL, 50002), - new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002), + new Server("btc.aftrek.org", Server.ConnectionType.SSL, 50002), + new Server("btc.hodler.ninja", Server.ConnectionType.SSL, 50002), new Server("btc.ocf.sh", Server.ConnectionType.SSL, 50002), new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002), new Server("caleb.vegas", Server.ConnectionType.SSL, 50002), + new Server("d762li0k0g.d.firewalla.org", Server.ConnectionType.SSL, 50002), + new Server("de.poiuty.com", Server.ConnectionType.SSL, 50002), + new Server("dijon.anties.org", Server.ConnectionType.SSL, 50002), new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002), - new Server("electrum.bhoovd.com", Server.ConnectionType.SSL, 50002), new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002), - new Server("electrum.bitcoinlizard.net", Server.ConnectionType.SSL, 50002), - new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002), + new Server("electrum.bitrefill.com", Server.ConnectionType.SSL, 50002), + new Server("electrum.brainshome.de", Server.ConnectionType.SSL, 50002), new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002), - new Server("electrum.exan.tech", Server.ConnectionType.SSL, 50002), + new Server("electrum.kcicom.net", Server.ConnectionType.SSL, 50002), new Server("electrum.kendigisland.xyz", Server.ConnectionType.SSL, 50002), - new Server("electrum.mmitech.info", Server.ConnectionType.SSL, 50002), - new Server("electrum.petrkr.net", Server.ConnectionType.SSL, 50002), - new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002), new Server("electrum.thomasfischbach.de", Server.ConnectionType.SSL, 50002), + new Server("electrum-btc.leblancnet.us", Server.ConnectionType.SSL, 50002), new Server("electrum0.snel.it", Server.ConnectionType.SSL, 50002), - new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 50002), - new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 50002), - new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 50002), - new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002), + new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20000), + new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20000), + new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20000), + new Server("electrumx.blockfinance-eco.li", Server.ConnectionType.SSL, 50002), + new Server("electrumx.indoor.app", Server.ConnectionType.SSL, 50002), + new Server("electrumx.iodata.org", Server.ConnectionType.SSL, 50002), new Server("electrumx-core.1209k.com", Server.ConnectionType.SSL, 50002), new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002), - new Server("ex03.axalgo.com", Server.ConnectionType.SSL, 50002), - new Server("ex05.axalgo.com", Server.ConnectionType.SSL, 50002), - new Server("ex07.axalgo.com", Server.ConnectionType.SSL, 50002), + new Server("exs.dyshek.org", Server.ConnectionType.SSL, 50002), new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002), - new Server("fulcrum.grey.pw", Server.ConnectionType.SSL, 50002), - new Server("fulcrum.sethforprivacy.com", Server.ConnectionType.SSL, 51002), new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002), + new Server("hodl.artyomk13.me", Server.ConnectionType.SSL, 50002), new Server("hodlers.beer", Server.ConnectionType.SSL, 50002), new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002), new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002), + new Server("kittyserver.ddnsfree.com", Server.ConnectionType.SSL, 50002), + new Server("lille.anties.org", Server.ConnectionType.SSL, 50002), + new Server("marseille.anties.org", Server.ConnectionType.SSL, 50002), new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002), new Server("osr1ex1.compumundohipermegared.one", Server.ConnectionType.SSL, 50002), - new Server("smmalis37.ddns.net", Server.ConnectionType.SSL, 50002), - new Server("ulrichard.ch", Server.ConnectionType.SSL, 50002), - new Server("vmd104012.contaboserver.net", Server.ConnectionType.SSL, 50002), - new Server("vmd104014.contaboserver.net", Server.ConnectionType.SSL, 50002), + new Server("paris.anties.org", Server.ConnectionType.SSL, 50002), + new Server("ragtor.duckdns.org", Server.ConnectionType.SSL, 50002), + new Server("stavver.dyshek.org", Server.ConnectionType.SSL, 50002), new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002), - new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002), - new Server("vmd84592.contaboserver.net", Server.ConnectionType.SSL, 50002), new Server("xtrum.com", Server.ConnectionType.SSL, 50002) ); } @@ -125,11 +111,7 @@ public class Bitcoin extends Bitcoiny { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP) - return OLD_FEE_AMOUNT; - - return NEW_FEE_AMOUNT; + return this.getFeeCeiling(); } }, TEST3 { @@ -141,12 +123,17 @@ public class Bitcoin extends Bitcoiny { @Override public Collection getServers() { return Arrays.asList( - new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002), - new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002), - new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002), - new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001), + new Server("bitcoin.devmole.eu", Server.ConnectionType.TCP, 5000), + new Server("bitcoin.stagemole.eu", Server.ConnectionType.TCP, 5000), + new Server("blockstream.info", Server.ConnectionType.SSL, 993), + new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002), + new Server("electrum1.cipig.net", Server.ConnectionType.TCP, 10068), + new Server("electrum2.cipig.net", Server.ConnectionType.TCP, 10068), + new Server("electrum3.cipig.net", Server.ConnectionType.TCP, 10068), new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002), - new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012) + new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012), + new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002), + new Server("v22019051929289916.bestsrv.de", Server.ConnectionType.SSL, 50002) ); } @@ -186,6 +173,16 @@ public class Bitcoin extends Bitcoiny { } }; + private long feeCeiling = NEW_FEE_AMOUNT; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -199,7 +196,7 @@ public class Bitcoin extends Bitcoiny { // Constructors and instance private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, bitcoinjContext.getFeePerKb()); this.bitcoinNet = bitcoinNet; LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name())); @@ -244,6 +241,16 @@ public class Bitcoin extends Bitcoiny { return this.bitcoinNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.bitcoinNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.bitcoinNet.setFeeCeiling( fee ); + } /** * Returns bitcoinj transaction sending amount to recipient using 20 sat/byte fee. * diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index 1749cee9..1ae70fe9 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoiny.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -11,6 +11,7 @@ import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.wallet.DeterministicKeyChain; +import org.bitcoinj.wallet.KeyChain; import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.wallet.Wallet; import org.qortal.api.model.SimpleForeignTransaction; @@ -52,12 +53,15 @@ public abstract class Bitcoiny implements ForeignBlockchain { /** Byte offset into raw block headers to block timestamp. */ private static final int TIMESTAMP_OFFSET = 4 + 32 + 32; + protected Coin feePerKb; + // Constructors and instance - protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) { + protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode, Coin feePerKb) { this.blockchainProvider = blockchainProvider; this.bitcoinjContext = bitcoinjContext; this.currencyCode = currencyCode; + this.feePerKb = feePerKb; this.params = this.bitcoinjContext.getParams(); } @@ -166,7 +170,11 @@ public abstract class Bitcoiny implements ForeignBlockchain { /** Returns fee per transaction KB. To be overridden for testnet/regtest. */ public Coin getFeePerKb() { - return this.bitcoinjContext.getFeePerKb(); + return this.feePerKb; + } + + public void setFeePerKb(Coin feePerKb) { + this.feePerKb = feePerKb; } /** Returns minimum order size in sats. To be overridden for coins that need to restrict order size. */ @@ -504,7 +512,7 @@ public abstract class Bitcoiny implements ForeignBlockchain { List candidates = this.getSpendingCandidateAddresses(key58); - for(DeterministicKey key : getWalletKeys(key58)) { + for(DeterministicKey key : getOldWalletKeys(key58)) { infos.add(buildAddressInfo(key, candidates)); } @@ -591,11 +599,23 @@ public abstract class Bitcoiny implements ForeignBlockchain { } } - private List getWalletKeys(String key58) throws ForeignBlockchainException { + /** + * Get Old Wallet Keys + * + * Get wallet keys using the old key generation algorithm. This is used for diagnosing and repairing wallets + * created before 2024. + * + * @param masterPrivateKey + * + * @return the keys + * + * @throws ForeignBlockchainException + */ + private List getOldWalletKeys(String masterPrivateKey) throws ForeignBlockchainException { synchronized (this) { Context.propagate(bitcoinjContext); - Wallet wallet = walletFromDeterministicKey58(key58); + Wallet wallet = walletFromDeterministicKey58(masterPrivateKey); DeterministicKeyChain keyChain = wallet.getActiveKeyChain(); keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT); @@ -720,7 +740,7 @@ public abstract class Bitcoiny implements ForeignBlockchain { } /** - * Returns first unused receive address given 'm' BIP32 key. + * Returns first unused receive address given a BIP32 key. * * @param key58 BIP32/HD extended Bitcoin private/public key * @return P2PKH address @@ -732,68 +752,22 @@ public abstract class Bitcoiny implements ForeignBlockchain { Wallet wallet = walletFromDeterministicKey58(key58); DeterministicKeyChain keyChain = wallet.getActiveKeyChain(); - keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT); - keyChain.maybeLookAhead(); - - final int keyChainPathSize = keyChain.getAccountPath().size(); - List keys = new ArrayList<>(keyChain.getLeafKeys()); - - int ki = 0; do { - for (; ki < keys.size(); ++ki) { - DeterministicKey dKey = keys.get(ki); - List dKeyPath = dKey.getPath(); + // the next receive funds address + Address address = Address.fromKey(this.params, keyChain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS), ScriptType.P2PKH); - // If keyChain is based on 'm', then make sure dKey is m/0/ki - i.e. a 'receive' address, not 'change' (m/1/ki) - if (dKeyPath.size() != keyChainPathSize + 2 || dKeyPath.get(dKeyPath.size() - 2) != ChildNumber.ZERO) - continue; + // if zero transactions, return address + if( 0 == getAddressTransactions(ScriptBuilder.createOutputScript(address).getProgram(), true).size() ) + return address.toString(); - // Check unspent - Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH); - byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); - - List unspentOutputs = this.blockchainProvider.getUnspentOutputs(script, false); - - /* - * If there are no unspent outputs then either: - * a) all the outputs have been spent - * b) address has never been used - * - * For case (a) we want to remember not to check this address (key) again. - */ - - if (unspentOutputs.isEmpty()) { - // If this is a known key that has been spent before, then we can skip asking for transaction history - if (this.spentKeys.contains(dKey)) { - wallet.getActiveKeyChain().markKeyAsUsed(dKey); - continue; - } - - // Ask for transaction history - if it's empty then key has never been used - List historicTransactionHashes = this.blockchainProvider.getAddressTransactions(script, false); - - if (!historicTransactionHashes.isEmpty()) { - // Fully spent key - case (a) - this.spentKeys.add(dKey); - wallet.getActiveKeyChain().markKeyAsUsed(dKey); - continue; - } - - // Key never been used - case (b) - return address.toString(); - } - - // Key has unspent outputs, hence used, so no good to us - this.spentKeys.remove(dKey); - } - - // Generate some more keys - keys.addAll(generateMoreKeys(keyChain)); - - // Process new keys + // else try the next receive funds address } while (true); } + public abstract long getFeeCeiling(); + + public abstract void setFeeCeiling(long fee); + // UTXOProvider support static class WalletAwareUTXOProvider implements UTXOProvider { @@ -1047,4 +1021,52 @@ public abstract class Bitcoiny implements ForeignBlockchain { return Wallet.fromWatchingKeyB58(this.params, key58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); } + /** + * Repair Wallet + * + * Repair wallets generated before 2024 by moving all the address balances to the first address. + * + * @param privateMasterKey + * + * @return the transaction Id of the spend operation that moves the balances or the exception name if an exception + * is thrown + * + * @throws ForeignBlockchainException + */ + public String repairOldWallet(String privateMasterKey) throws ForeignBlockchainException { + + // create a deterministic wallet to satisfy the bitcoinj API + Wallet wallet = Wallet.createDeterministic(this.bitcoinjContext, ScriptType.P2PKH); + + // use the blockchain resources of this instance for UTXO provision + wallet.setUTXOProvider(new BitcoinyUTXOProvider( this )); + + // import in each that is generated using the old key generation algorithm + List walletKeys = getOldWalletKeys(privateMasterKey); + + for( DeterministicKey key : walletKeys) { + wallet.importKey(ECKey.fromPrivate(key.getPrivKey())); + } + + // get the primary receive address + Address firstAddress = Address.fromKey(this.params, walletKeys.get(0), ScriptType.P2PKH); + + // send all the imported coins to the primary receive address + SendRequest sendRequest = SendRequest.emptyWallet(firstAddress); + sendRequest.feePerKb = this.getFeePerKb(); + + try { + // allow the wallet to build the send request transaction and broadcast + wallet.completeTx(sendRequest); + broadcastTransaction(sendRequest.tx); + + // return the transaction Id + return sendRequest.tx.getTxId().toString(); + } + catch( Exception e ) { + // log error and return exception name + LOGGER.error(e.getMessage(), e); + return e.getClass().getSimpleName(); + } + } } diff --git a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java index d8b4f653..238eff38 100644 --- a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java +++ b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java @@ -3,6 +3,7 @@ package org.qortal.crosschain; import cash.z.wallet.sdk.rpc.CompactFormats.CompactBlock; import java.util.List; +import java.util.Set; public abstract class BitcoinyBlockchainProvider { @@ -59,4 +60,11 @@ public abstract class BitcoinyBlockchainProvider { /** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */ public abstract void broadcastTransaction(byte[] rawTransaction) throws ForeignBlockchainException; + public abstract Set getServers(); + + public abstract List getRemainingServers(); + + public abstract Set getUselessServers(); + + public abstract ChainableServer getCurrentServer(); } diff --git a/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java new file mode 100644 index 00000000..df596de4 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java @@ -0,0 +1,80 @@ +package org.qortal.crosschain; + +import org.bitcoinj.core.*; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class BitcoinyUTXOProvider + * + * Uses Bitcoiny resources for UTXO provision. + */ +public class BitcoinyUTXOProvider implements UTXOProvider { + + private Bitcoiny bitcoiny; + + public BitcoinyUTXOProvider(Bitcoiny bitcoiny) { + this.bitcoiny = bitcoiny; + } + + @Override + public List getOpenTransactionOutputs(List keys) throws UTXOProviderException { + try { + List utxos = new ArrayList<>(); + + for( ECKey key : keys) { + Address address = Address.fromKey(this.bitcoiny.params, key, Script.ScriptType.P2PKH); + byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); + + // collection UTXO's for all confirmed unspent outputs + for (UnspentOutput output : this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false)) { + utxos.add(toUTXO(output)); + } + } + return utxos; + } catch (ForeignBlockchainException e) { + throw new UTXOProviderException(e); + } + } + + /** + * Convert Unspent Output to a UTXO + * + * @param unspentOutput + * + * @return the UTXO + * + * @throws ForeignBlockchainException + */ + private UTXO toUTXO(UnspentOutput unspentOutput) throws ForeignBlockchainException { + List transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash); + + TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index); + + return new UTXO( + Sha256Hash.wrap(unspentOutput.hash), + unspentOutput.index, + Coin.valueOf(unspentOutput.value), + unspentOutput.height, + false, + transactionOutput.getScriptPubKey() + ); + } + + @Override + public int getChainHeadHeight() throws UTXOProviderException { + try { + return this.bitcoiny.blockchainProvider.getCurrentHeight(); + } catch (ForeignBlockchainException e) { + throw new UTXOProviderException(e); + } + } + + @Override + public NetworkParameters getParams() { + return this.bitcoiny.params; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/ChainableServer.java b/src/main/java/org/qortal/crosschain/ChainableServer.java new file mode 100644 index 00000000..ac29e5f9 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/ChainableServer.java @@ -0,0 +1,15 @@ +package org.qortal.crosschain; + +public interface ChainableServer { + public void addResponseTime(long responseTime); + + public long averageResponseTime(); + + public String getHostName(); + + public int getPort(); + + public ConnectionType getConnectionType(); + + public enum ConnectionType {TCP, SSL} +} diff --git a/src/main/java/org/qortal/crosschain/Digibyte.java b/src/main/java/org/qortal/crosschain/Digibyte.java index 4e725e89..f0a31087 100644 --- a/src/main/java/org/qortal/crosschain/Digibyte.java +++ b/src/main/java/org/qortal/crosschain/Digibyte.java @@ -7,7 +7,7 @@ import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; import org.libdohj.params.DigibyteMainNetParams; import org.qortal.crosschain.ElectrumX.Server; -import org.qortal.crosschain.ElectrumX.Server.ConnectionType; +import org.qortal.crosschain.ChainableServer.ConnectionType; import org.qortal.settings.Settings; import java.util.Arrays; @@ -46,10 +46,6 @@ public class Digibyte extends Bitcoiny { // Servers chosen on NO BASIS WHATSOEVER from various sources! // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb new Server("electrum.qortal.link", Server.ConnectionType.SSL, 55002), - new Server("electrum1-dgb.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum2-dgb.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum3-dgb.qortal.online", Server.ConnectionType.SSL, 40002), - new Server("electrum4-dgb.qortal.online", Server.ConnectionType.SSL, 40002), new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20059), new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20059), new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20059) @@ -63,8 +59,7 @@ public class Digibyte extends Bitcoiny { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -114,6 +109,16 @@ public class Digibyte extends Bitcoiny { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -127,7 +132,7 @@ public class Digibyte extends Bitcoiny { // Constructors and instance private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.digibyteNet = digibyteNet; LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name())); @@ -156,11 +161,6 @@ public class Digibyte extends Bitcoiny { // Actual useful methods for use by other classes - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -177,4 +177,14 @@ public class Digibyte extends Bitcoiny { return this.digibyteNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.digibyteNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.digibyteNet.setFeeCeiling( fee ); + } } diff --git a/src/main/java/org/qortal/crosschain/Dogecoin.java b/src/main/java/org/qortal/crosschain/Dogecoin.java index 93941c41..dff98b1c 100644 --- a/src/main/java/org/qortal/crosschain/Dogecoin.java +++ b/src/main/java/org/qortal/crosschain/Dogecoin.java @@ -6,7 +6,7 @@ import org.bitcoinj.core.NetworkParameters; import org.libdohj.params.DogecoinMainNetParams; import org.libdohj.params.DogecoinTestNet3Params; import org.qortal.crosschain.ElectrumX.Server; -import org.qortal.crosschain.ElectrumX.Server.ConnectionType; +import org.qortal.crosschain.ChainableServer.ConnectionType; import org.qortal.settings.Settings; import java.util.Arrays; @@ -45,11 +45,8 @@ public class Dogecoin extends Bitcoiny { return Arrays.asList( // Servers chosen on NO BASIS WHATSOEVER from various sources! // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge + new Server("dogecoin.stackwallet.com", Server.ConnectionType.SSL, 50022), new Server("electrum.qortal.link", Server.ConnectionType.SSL, 54002), - new Server("electrum1-doge.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum2-doge.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum3-doge.qortal.online", Server.ConnectionType.SSL, 30002), - new Server("electrum4-doge.qortal.online", Server.ConnectionType.SSL, 30002), new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20060), new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20060), new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20060) @@ -63,8 +60,7 @@ public class Dogecoin extends Bitcoiny { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -114,6 +110,16 @@ public class Dogecoin extends Bitcoiny { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -127,7 +133,7 @@ public class Dogecoin extends Bitcoiny { // Constructors and instance private Dogecoin(DogecoinNet dogecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.dogecoinNet = dogecoinNet; LOGGER.info(() -> String.format("Starting Dogecoin support using %s", this.dogecoinNet.name())); @@ -156,11 +162,6 @@ public class Dogecoin extends Bitcoiny { // Actual useful methods for use by other classes - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -177,4 +178,14 @@ public class Dogecoin extends Bitcoiny { return this.dogecoinNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.dogecoinNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.dogecoinNet.setFeeCeiling( fee ); + } } diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java index 5915ba04..27e140e2 100644 --- a/src/main/java/org/qortal/crosschain/ElectrumX.java +++ b/src/main/java/org/qortal/crosschain/ElectrumX.java @@ -43,12 +43,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider { private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported"; private static final int RESPONSE_TIME_READINGS = 5; - private static final long MAX_AVG_RESPONSE_TIME = 1000L; // ms + private static final long MAX_AVG_RESPONSE_TIME = 2000L; // ms - public static class Server { + public static class Server implements ChainableServer { String hostname; - public enum ConnectionType { TCP, SSL } ConnectionType connectionType; int port; @@ -60,6 +59,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider { this.port = port; } + @Override public void addResponseTime(long responseTime) { while (this.responseTimes.size() > RESPONSE_TIME_READINGS) { this.responseTimes.remove(0); @@ -67,6 +67,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider { this.responseTimes.add(responseTime); } + @Override public long averageResponseTime() { if (this.responseTimes.size() < RESPONSE_TIME_READINGS) { // Not enough readings yet @@ -79,6 +80,21 @@ public class ElectrumX extends BitcoinyBlockchainProvider { return 0L; } + @Override + public String getHostName() { + return this.hostname; + } + + @Override + public int getPort() { + return this.port; + } + + @Override + public ConnectionType getConnectionType() { + return this.connectionType; + } + @Override public boolean equals(Object other) { if (other == this) @@ -104,9 +120,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider { return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port); } } - private Set servers = new HashSet<>(); - private List remainingServers = new ArrayList<>(); - private Set uselessServers = Collections.synchronizedSet(new HashSet<>()); + private Set servers = new HashSet<>(); + private List remainingServers = new ArrayList<>(); + private Set uselessServers = Collections.synchronizedSet(new HashSet<>()); private final String netId; private final String expectedGenesisHash; @@ -114,7 +130,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider { private Bitcoiny blockchain; private final Object serverLock = new Object(); - private Server currentServer; + private ChainableServer currentServer; private Socket socket; private Scanner scanner; private int nextId = 1; @@ -638,7 +654,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider { if (!this.remainingServers.isEmpty()) { long averageResponseTime = this.currentServer.averageResponseTime(); if (averageResponseTime > MAX_AVG_RESPONSE_TIME) { - LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname); + LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName()); this.closeServer(); break; } @@ -663,20 +679,20 @@ public class ElectrumX extends BitcoinyBlockchainProvider { return true; while (!this.remainingServers.isEmpty()) { - Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size())); + ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size())); LOGGER.trace(() -> String.format("Connecting to %s", server)); try { - SocketAddress endpoint = new InetSocketAddress(server.hostname, server.port); + SocketAddress endpoint = new InetSocketAddress(server.getHostName(), server.getPort()); int timeout = 5000; // ms this.socket = new Socket(); this.socket.connect(endpoint, timeout); this.socket.setTcpNoDelay(true); - if (server.connectionType == Server.ConnectionType.SSL) { + if (server.getConnectionType() == Server.ConnectionType.SSL) { SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory(); - this.socket = factory.createSocket(this.socket, server.hostname, server.port, true); + this.socket = factory.createSocket(this.socket, server.getHostName(), server.getPort(), true); } this.scanner = new Scanner(this.socket.getInputStream()); @@ -832,7 +848,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider { * Closes connection to server if it is currently connected server. * @param server */ - private void closeServer(Server server) { + private void closeServer(ChainableServer server) { synchronized (this.serverLock) { if (this.currentServer == null || !this.currentServer.equals(server)) return; @@ -857,4 +873,24 @@ public class ElectrumX extends BitcoinyBlockchainProvider { } } + @Override + public Set getServers() { + LOGGER.info("getting servers"); + return servers; + } + + @Override + public List getRemainingServers() { + return remainingServers; + } + + @Override + public Set getUselessServers() { + return uselessServers; + } + + @Override + public ChainableServer getCurrentServer() { + return currentServer; + } } diff --git a/src/main/java/org/qortal/crosschain/Litecoin.java b/src/main/java/org/qortal/crosschain/Litecoin.java index a0f7c1cb..f13c1043 100644 --- a/src/main/java/org/qortal/crosschain/Litecoin.java +++ b/src/main/java/org/qortal/crosschain/Litecoin.java @@ -7,7 +7,7 @@ import org.libdohj.params.LitecoinMainNetParams; import org.libdohj.params.LitecoinRegTestParams; import org.libdohj.params.LitecoinTestNet3Params; import org.qortal.crosschain.ElectrumX.Server; -import org.qortal.crosschain.ElectrumX.Server.ConnectionType; +import org.qortal.crosschain.ChainableServer.ConnectionType; import org.qortal.settings.Settings; import java.util.Arrays; @@ -45,13 +45,9 @@ public class Litecoin extends Bitcoiny { return Arrays.asList( // Servers chosen on NO BASIS WHATSOEVER from various sources! // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc - new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002), - new Server("electrum1-ltc.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum2-ltc.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum3-ltc.qortal.online", Server.ConnectionType.SSL, 20002), - new Server("electrum4-ltc.qortal.online", Server.ConnectionType.SSL, 20002), new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443), new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002), + new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002), new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002), new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063), new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063), @@ -67,8 +63,7 @@ public class Litecoin extends Bitcoiny { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -80,9 +75,7 @@ public class Litecoin extends Bitcoiny { @Override public Collection getServers() { return Arrays.asList( - new Server("electrum-ltc.bysh.me", Server.ConnectionType.TCP, 51001), new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 51002), - new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 51001), new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 51002) ); } @@ -123,6 +116,16 @@ public class Litecoin extends Bitcoiny { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -136,7 +139,7 @@ public class Litecoin extends Bitcoiny { // Constructors and instance private Litecoin(LitecoinNet litecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.litecoinNet = litecoinNet; LOGGER.info(() -> String.format("Starting Litecoin support using %s", this.litecoinNet.name())); @@ -165,12 +168,6 @@ public class Litecoin extends Bitcoiny { // Actual useful methods for use by other classes - /** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */ - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -187,4 +184,14 @@ public class Litecoin extends Bitcoiny { return this.litecoinNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.litecoinNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.litecoinNet.setFeeCeiling( fee ); + } } diff --git a/src/main/java/org/qortal/crosschain/PirateChain.java b/src/main/java/org/qortal/crosschain/PirateChain.java index 6587baca..5475c929 100644 --- a/src/main/java/org/qortal/crosschain/PirateChain.java +++ b/src/main/java/org/qortal/crosschain/PirateChain.java @@ -13,7 +13,7 @@ import org.libdohj.params.PirateChainMainNetParams; import org.qortal.api.model.crosschain.PirateChainSendRequest; import org.qortal.controller.PirateChainWalletController; import org.qortal.crosschain.PirateLightClient.Server; -import org.qortal.crosschain.PirateLightClient.Server.ConnectionType; +import org.qortal.crosschain.ChainableServer.ConnectionType; import org.qortal.crypto.Crypto; import org.qortal.settings.Settings; import org.qortal.transform.TransformationException; @@ -51,12 +51,12 @@ public class PirateChain extends Bitcoiny { public Collection getServers() { return Arrays.asList( // Servers chosen on NO BASIS WHATSOEVER from various sources! + new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443), new Server("wallet-arrr1.qortal.online", Server.ConnectionType.SSL, 443), new Server("wallet-arrr2.qortal.online", Server.ConnectionType.SSL, 443), new Server("wallet-arrr3.qortal.online", Server.ConnectionType.SSL, 443), new Server("wallet-arrr4.qortal.online", Server.ConnectionType.SSL, 443), - new Server("wallet-arrr5.qortal.online", Server.ConnectionType.SSL, 443), - new Server("lightd.pirate.black", Server.ConnectionType.SSL, 443) + new Server("wallet-arrr5.qortal.online", Server.ConnectionType.SSL, 443) ); } @@ -67,8 +67,7 @@ public class PirateChain extends Bitcoiny { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -118,6 +117,16 @@ public class PirateChain extends Bitcoiny { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -131,7 +140,7 @@ public class PirateChain extends Bitcoiny { // Constructors and instance private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.pirateChainNet = pirateChainNet; LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name())); @@ -160,12 +169,6 @@ public class PirateChain extends Bitcoiny { // Actual useful methods for use by other classes - /** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */ - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -182,6 +185,16 @@ public class PirateChain extends Bitcoiny { return this.pirateChainNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.pirateChainNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.pirateChainNet.setFeeCeiling( fee ); + } /** * Returns confirmed balance, based on passed payment script. *

diff --git a/src/main/java/org/qortal/crosschain/PirateLightClient.java b/src/main/java/org/qortal/crosschain/PirateLightClient.java index be4370a0..ae7c3cc1 100644 --- a/src/main/java/org/qortal/crosschain/PirateLightClient.java +++ b/src/main/java/org/qortal/crosschain/PirateLightClient.java @@ -30,10 +30,9 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { private static final int RESPONSE_TIME_READINGS = 5; private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms - public static class Server { + public static class Server implements ChainableServer{ String hostname; - public enum ConnectionType { TCP, SSL } ConnectionType connectionType; int port; @@ -64,6 +63,21 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { return 0L; } + @Override + public String getHostName() { + return this.hostname; + } + + @Override + public int getPort() { + return this.port; + } + + @Override + public ChainableServer.ConnectionType getConnectionType() { + return this.connectionType; + } + @Override public boolean equals(Object other) { if (other == this) @@ -89,9 +103,9 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port); } } - private Set servers = new HashSet<>(); - private List remainingServers = new ArrayList<>(); - private Set uselessServers = Collections.synchronizedSet(new HashSet<>()); + private Set servers = new HashSet<>(); + private List remainingServers = new ArrayList<>(); + private Set uselessServers = Collections.synchronizedSet(new HashSet<>()); private final String netId; private final String expectedGenesisHash; @@ -99,7 +113,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { private Bitcoiny blockchain; private final Object serverLock = new Object(); - private Server currentServer; + private ChainableServer currentServer; private ManagedChannel channel; private int nextId = 1; @@ -525,6 +539,24 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error code from Pirate Chain broadcastTransaction gRPC: %d", sendResponse.getErrorCode())); } + @Override + public Set getServers() { + return this.servers; + } + + @Override + public List getRemainingServers() { + return this.remainingServers; + } + + @Override + public Set getUselessServers() { + return this.uselessServers; + } + + @Override + public ChainableServer getCurrentServer() { return this.currentServer; } + // Class-private utility methods @@ -544,7 +576,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { if (!this.remainingServers.isEmpty()) { long averageResponseTime = this.currentServer.averageResponseTime(); if (averageResponseTime > MAX_AVG_RESPONSE_TIME) { - LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname); + LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.getHostName()); this.closeServer(); continue; } @@ -568,11 +600,11 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { return true; while (!this.remainingServers.isEmpty()) { - Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size())); + ChainableServer server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size())); LOGGER.trace(() -> String.format("Connecting to %s", server)); try { - this.channel = ManagedChannelBuilder.forAddress(server.hostname, server.port).build(); + this.channel = ManagedChannelBuilder.forAddress(server.getHostName(), server.getPort()).build(); CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel); LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build()); @@ -604,7 +636,7 @@ public class PirateLightClient extends BitcoinyBlockchainProvider { * Closes connection to server if it is currently connected server. * @param server */ - private void closeServer(Server server) { + private void closeServer(ChainableServer server) { synchronized (this.serverLock) { if (this.currentServer == null || !this.currentServer.equals(server) || this.channel == null) { return; diff --git a/src/main/java/org/qortal/crosschain/Ravencoin.java b/src/main/java/org/qortal/crosschain/Ravencoin.java index 6b267a00..cd98fb69 100644 --- a/src/main/java/org/qortal/crosschain/Ravencoin.java +++ b/src/main/java/org/qortal/crosschain/Ravencoin.java @@ -7,7 +7,7 @@ import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; import org.libdohj.params.RavencoinMainNetParams; import org.qortal.crosschain.ElectrumX.Server; -import org.qortal.crosschain.ElectrumX.Server.ConnectionType; +import org.qortal.crosschain.ChainableServer.ConnectionType; import org.qortal.settings.Settings; import java.util.Arrays; @@ -46,10 +46,6 @@ public class Ravencoin extends Bitcoiny { // Servers chosen on NO BASIS WHATSOEVER from various sources! // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn new Server("electrum.qortal.link", Server.ConnectionType.SSL, 56002), - new Server("electrum1-rvn.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum2-rvn.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum3-rvn.qortal.online", Server.ConnectionType.SSL, 50002), - new Server("electrum4-rvn.qortal.online", Server.ConnectionType.SSL, 50002), new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20051), new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20051), new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20051), @@ -65,8 +61,7 @@ public class Ravencoin extends Bitcoiny { @Override public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; + return this.getFeeCeiling(); } }, TEST3 { @@ -116,6 +111,16 @@ public class Ravencoin extends Bitcoiny { } }; + private long feeCeiling = MAINNET_FEE; + + public long getFeeCeiling() { + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + this.feeCeiling = feeCeiling; + } + public abstract NetworkParameters getParams(); public abstract Collection getServers(); public abstract String getGenesisHash(); @@ -129,7 +134,7 @@ public class Ravencoin extends Bitcoiny { // Constructors and instance private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); + super(blockchain, bitcoinjContext, currencyCode, DEFAULT_FEE_PER_KB); this.ravencoinNet = ravencoinNet; LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name())); @@ -158,11 +163,6 @@ public class Ravencoin extends Bitcoiny { // Actual useful methods for use by other classes - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - @Override public long getMinimumOrderAmount() { return MINIMUM_ORDER_AMOUNT; @@ -179,4 +179,14 @@ public class Ravencoin extends Bitcoiny { return this.ravencoinNet.getP2shFee(timestamp); } + @Override + public long getFeeCeiling() { + return this.ravencoinNet.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.ravencoinNet.setFeeCeiling( fee ); + } } diff --git a/src/main/java/org/qortal/crosschain/ServerConfigurationInfo.java b/src/main/java/org/qortal/crosschain/ServerConfigurationInfo.java new file mode 100644 index 00000000..89674ba5 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/ServerConfigurationInfo.java @@ -0,0 +1,60 @@ +package org.qortal.crosschain; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.List; +import java.util.Objects; + +@XmlAccessorType(XmlAccessType.FIELD) +public class ServerConfigurationInfo { + + private List servers; + private List remainingServers; + private List uselessServers; + + public ServerConfigurationInfo() { + } + + public ServerConfigurationInfo( + List servers, + List remainingServers, + List uselessServers) { + this.servers = servers; + this.remainingServers = remainingServers; + this.uselessServers = uselessServers; + } + + public List getServers() { + return servers; + } + + public List getRemainingServers() { + return remainingServers; + } + + public List getUselessServers() { + return uselessServers; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServerConfigurationInfo that = (ServerConfigurationInfo) o; + return Objects.equals(servers, that.servers) && Objects.equals(remainingServers, that.remainingServers) && Objects.equals(uselessServers, that.uselessServers); + } + + @Override + public int hashCode() { + return Objects.hash(servers, remainingServers, uselessServers); + } + + @Override + public String toString() { + return "ServerConfigurationInfo{" + + "servers=" + servers + + ", remainingServers=" + remainingServers + + ", uselessServers=" + uselessServers + + '}'; + } +} diff --git a/src/main/java/org/qortal/crosschain/ServerInfo.java b/src/main/java/org/qortal/crosschain/ServerInfo.java new file mode 100644 index 00000000..0efe510b --- /dev/null +++ b/src/main/java/org/qortal/crosschain/ServerInfo.java @@ -0,0 +1,74 @@ +package org.qortal.crosschain; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Objects; + +@XmlAccessorType(XmlAccessType.FIELD) +public class ServerInfo { + + private long averageResponseTime; + + private String hostName; + + private int port; + + private String connectionType; + + private boolean isCurrent; + + public ServerInfo() { + } + + public ServerInfo(long averageResponseTime, String hostName, int port, String connectionType, boolean isCurrent) { + this.averageResponseTime = averageResponseTime; + this.hostName = hostName; + this.port = port; + this.connectionType = connectionType; + this.isCurrent = isCurrent; + } + + public long getAverageResponseTime() { + return averageResponseTime; + } + + public String getHostName() { + return hostName; + } + + public int getPort() { + return port; + } + + public String getConnectionType() { + return connectionType; + } + + public boolean isCurrent() { + return isCurrent; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServerInfo that = (ServerInfo) o; + return averageResponseTime == that.averageResponseTime && port == that.port && isCurrent == that.isCurrent && Objects.equals(hostName, that.hostName) && Objects.equals(connectionType, that.connectionType); + } + + @Override + public int hashCode() { + return Objects.hash(averageResponseTime, hostName, port, connectionType, isCurrent); + } + + @Override + public String toString() { + return "ServerInfo{" + + "averageResponseTime=" + averageResponseTime + + ", hostName='" + hostName + '\'' + + ", port=" + port + + ", connectionType='" + connectionType + '\'' + + ", isCurrent=" + isCurrent + + '}'; + } +} diff --git a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java index f14efae8..3643e552 100644 --- a/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java +++ b/src/main/java/org/qortal/crypto/TrustlessSSLSocketFactory.java @@ -1,33 +1,33 @@ package org.qortal.crypto; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; +import javax.net.ssl.*; import java.security.cert.X509Certificate; public abstract class TrustlessSSLSocketFactory { - // Create a trust manager that does not validate certificate chains + /** + * Creates a SSLSocketFactory that ignore certificate chain validation because ElectrumX servers use mostly + * self signed certificates. + */ private static final TrustManager[] TRUSTLESS_MANAGER = new TrustManager[] { new X509TrustManager() { - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; + public X509Certificate[] getAcceptedIssuers() { + return null; } - - public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { + public void checkClientTrusted(X509Certificate[] certs, String authType) { } - - public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { + public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; - // Install the all-trusting trust manager + /** + * Install the all-trusting trust manager. + */ private static final SSLContext sc; static { try { - sc = SSLContext.getInstance("TLSv1.3"); + sc = SSLContext.getInstance("SSL"); sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom()); } catch (Exception e) { throw new RuntimeException(e); @@ -37,5 +37,4 @@ public abstract class TrustlessSSLSocketFactory { public static SSLSocketFactory getSocketFactory() { return sc.getSocketFactory(); } - } diff --git a/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java b/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java new file mode 100644 index 00000000..04c7a2a9 --- /dev/null +++ b/src/main/java/org/qortal/data/crosschain/AtomicTransactionData.java @@ -0,0 +1,32 @@ +package org.qortal.data.crosschain; + +import org.qortal.crosschain.BitcoinyTransaction; +import org.qortal.crosschain.TransactionHash; + +import java.util.List; +import java.util.Map; + +public class AtomicTransactionData { + public final TransactionHash hash; + public final Integer timestamp; + public final List inputs; + public final Map, Long> valueByAddress; + public final long totalAmount; + public final int size; + + public AtomicTransactionData( + TransactionHash hash, + Integer timestamp, + List inputs, + Map, Long> valueByAddress, + long totalAmount, + int size) { + + this.hash = hash; + this.timestamp = timestamp; + this.inputs = inputs; + this.valueByAddress = valueByAddress; + this.totalAmount = totalAmount; + this.size = size; + } +} diff --git a/src/main/java/org/qortal/data/crosschain/TransactionSummary.java b/src/main/java/org/qortal/data/crosschain/TransactionSummary.java new file mode 100644 index 00000000..ac67a2f6 --- /dev/null +++ b/src/main/java/org/qortal/data/crosschain/TransactionSummary.java @@ -0,0 +1,106 @@ +package org.qortal.data.crosschain; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@XmlAccessorType(XmlAccessType.FIELD) +public class TransactionSummary { + + private String atAddress; + private String p2shValue; + private String p2shAddress; + private String lockingHash; + private Integer lockingTimestamp; + private long lockingTotalAmount; + private long lockingFee; + private int lockingSize; + private String unlockingHash; + private Integer unlockingTimestamp; + private long unlockingTotalAmount; + private long unlockingFee; + private int unlockingSize; + + public TransactionSummary(){} + + public TransactionSummary( + String atAddress, + String p2shValue, + String p2shAddress, + String lockingHash, + Integer lockingTimestamp, + long lockingTotalAmount, + long lockingFee, + int lockingSize, + String unlockingHash, + Integer unlockingTimestamp, + long unlockingTotalAmount, + long unlockingFee, + int unlockingSize) { + + this.atAddress = atAddress; + this.p2shValue = p2shValue; + this.p2shAddress = p2shAddress; + this.lockingHash = lockingHash; + this.lockingTimestamp = lockingTimestamp; + this.lockingTotalAmount = lockingTotalAmount; + this.lockingFee = lockingFee; + this.lockingSize = lockingSize; + this.unlockingHash = unlockingHash; + this.unlockingTimestamp = unlockingTimestamp; + this.unlockingTotalAmount = unlockingTotalAmount; + this.unlockingFee = unlockingFee; + this.unlockingSize = unlockingSize; + } + + public String getAtAddress() { + return atAddress; + } + + public String getP2shValue() { + return p2shValue; + } + + public String getP2shAddress() { + return p2shAddress; + } + + public String getLockingHash() { + return lockingHash; + } + + public Integer getLockingTimestamp() { + return lockingTimestamp; + } + + public long getLockingTotalAmount() { + return lockingTotalAmount; + } + + public long getLockingFee() { + return lockingFee; + } + + public int getLockingSize() { + return lockingSize; + } + + public String getUnlockingHash() { + return unlockingHash; + } + + public Integer getUnlockingTimestamp() { + return unlockingTimestamp; + } + + public long getUnlockingTotalAmount() { + return unlockingTotalAmount; + } + + public long getUnlockingFee() { + return unlockingFee; + } + + public int getUnlockingSize() { + return unlockingSize; + } +} diff --git a/src/main/java/org/qortal/gui/SysTray.java b/src/main/java/org/qortal/gui/SysTray.java index abd433f3..74a68618 100644 --- a/src/main/java/org/qortal/gui/SysTray.java +++ b/src/main/java/org/qortal/gui/SysTray.java @@ -5,7 +5,6 @@ import org.apache.logging.log4j.Logger; import org.qortal.controller.Controller; import org.qortal.globalization.Translator; import org.qortal.settings.Settings; -import org.qortal.utils.RandomizeList; import org.qortal.utils.URLViewer; import javax.swing.*; @@ -140,14 +139,6 @@ public class SysTray { } }); - /* JMenuItem openUi = new JMenuItem(Translator.INSTANCE.translate("SysTray", "OPEN_UI")); - openUi.addActionListener(actionEvent -> { - destroyHiddenDialog(); - - new OpenUiWorker().execute(); - }); - menu.add(openUi); */ - JMenuItem openTimeCheck = new JMenuItem(Translator.INSTANCE.translate("SysTray", "CHECK_TIME_ACCURACY")); openTimeCheck.addActionListener(actionEvent -> { destroyHiddenDialog(); @@ -190,48 +181,6 @@ public class SysTray { return menu; } - static class OpenUiWorker extends SwingWorker { - @Override - protected Void doInBackground() { - List uiServers = new ArrayList<>(); - - String[] remoteUiServers = Settings.getInstance().getRemoteUiServers(); - uiServers.addAll(Arrays.asList(remoteUiServers)); - // Randomize remote servers - uiServers = RandomizeList.randomize(uiServers); - - // Prepend local servers - String[] localUiServers = Settings.getInstance().getLocalUiServers(); - uiServers.addAll(0, Arrays.asList(localUiServers)); - - // Check each server in turn before opening browser tab - int uiPort = Settings.getInstance().getUiServerPort(); - for (String uiServer : uiServers) { - InetSocketAddress socketAddress = new InetSocketAddress(uiServer, uiPort); - - // If we couldn't resolve try next - if (socketAddress.isUnresolved()) - continue; - - try (SocketChannel socketChannel = SocketChannel.open()) { - socketChannel.socket().connect(socketAddress, 100); - - // If we reach here, then socket connected to UI server! - URLViewer.openWebpage(new URL(String.format("http://%s:%d", uiServer, uiPort))); - - return null; - } catch (IOException e) { - // try next server - } catch (Exception e) { - LOGGER.error("Unable to open UI website in browser"); - return null; - } - } - - return null; - } - } - static class SynchronizeClockWorker extends SwingWorker { @Override protected Void doInBackground() { diff --git a/src/main/java/org/qortal/network/RNSNetwork.java b/src/main/java/org/qortal/network/RNSNetwork.java new file mode 100644 index 00000000..2472c891 --- /dev/null +++ b/src/main/java/org/qortal/network/RNSNetwork.java @@ -0,0 +1,425 @@ +package org.qortal.network; + +import java.io.IOException; +//import java.nio.channels.SelectionKey; +//import java.io.Paths; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.io.File; +import java.util.*; +//import java.util.function.BiConsumer; +//import java.util.function.Consumer; +//import java.util.function.Function; +//import java.util.concurrent.*; +//import java.util.concurrent.atomic.AtomicLong; + +//import org.qortal.data.network.PeerData; +import org.qortal.repository.DataException; +//import org.qortal.settings.Settings; +import org.qortal.settings.Settings; +//import org.qortal.utils.NTP; + +//import com.fasterxml.jackson.annotation.JsonGetter; + +import org.apache.commons.codec.binary.Hex; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +import io.reticulum.Reticulum; +import io.reticulum.Transport; +import io.reticulum.destination.Destination; +import io.reticulum.destination.DestinationType; +import io.reticulum.destination.Direction; +import io.reticulum.identity.Identity; +import io.reticulum.interfaces.ConnectionInterface; +import io.reticulum.destination.ProofStrategy; +import io.reticulum.transport.AnnounceHandler; +import static io.reticulum.constant.ReticulumConstant.CONFIG_FILE_NAME; +//import static io.reticulum.identity.IdentityKnownDestination.recall; +//import static io.reticulum.identity.IdentityKnownDestination.recallAppData; +//import static io.reticulum.destination.Direction.OUT; + +import lombok.extern.slf4j.Slf4j; +import lombok.Synchronized; +import io.reticulum.link.Link; +import io.reticulum.link.LinkStatus; +//import io.reticulum.packet.PacketReceipt; +import io.reticulum.packet.Packet; + +//import static io.reticulum.link.LinkStatus.ACTIVE; +import static io.reticulum.link.LinkStatus.CLOSED; +import static io.reticulum.link.LinkStatus.PENDING; +import static io.reticulum.link.LinkStatus.STALE; + +import static java.nio.charset.StandardCharsets.UTF_8; +//import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +//import org.qortal.network.Network.NetworkProcessor; +//import org.qortal.utils.ExecuteProduceConsume; +//import org.qortal.utils.NamedThreadFactory; + +//import java.time.Instant; + +//import org.qortal.network.RNSPeer; + +@Slf4j +public class RNSNetwork { + + static final String APP_NAME = "qortal"; + private Reticulum reticulum; + private Identity server_identity; + private Destination baseDestination; // service base (initially: anything node2node) + //private Destination dataDestination; // qdn services (eg. files like music, videos etc) + //private Destination liveDestination; // live/dynamic peer list (eg. video conferencing) + // the following should be retrieved from settings + private static Integer MAX_PEERS = 3; + private static Integer MIN_DESIRED_PEERS = 3; + //private final Integer MAX_PEERS = Settings.getInstance().getMaxReticulumPeers(); + //private final Integer MIN_DESIRED_PEERS = Settings.getInstance().getMinDesiredReticulumPeers(); + static final String defaultConfigPath = new String(".reticulum"); // if empty will look in Reticulums default paths + //private final String defaultConfigPath = Settings.getInstance().getDefaultConfigPathForReticulum(); + + //private static final Logger logger = LoggerFactory.getLogger(RNSNetwork.class); + + //private final List linkedPeers = Collections.synchronizedList(new ArrayList<>()); + //private List immutableLinkedPeers = Collections.emptyList(); + private final List linkedPeers = Collections.synchronizedList(new ArrayList<>()); + + //private final ExecuteProduceConsume rnsNetworkEPC; + private static final long NETWORK_EPC_KEEPALIVE = 1000L; // 1 second + private volatile boolean isShuttingDown = false; + private int totalThreadCount = 0; + + // TODO: settings - MaxReticulumPeers, MaxRNSNetworkThreadPoolSize (if needed) + + // Constructor + private RNSNetwork () { + try { + initConfig(defaultConfigPath); + reticulum = new Reticulum(defaultConfigPath); + log.info("reticulum instance created: {}", reticulum.toString()); + } catch (IOException e) { + log.error("unable to create Reticulum network", e); + } + + // Settings.getInstance().getMaxRNSNetworkThreadPoolSize(), // statically set to 5 below + //ExecutorService RNSNetworkExecutor = new ThreadPoolExecutor(1, + // 5, + // NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS, + // new SynchronousQueue(), + // new NamedThreadFactory("RNSNetwork-EPC")); + //rnsNetworkEPC = new RNSNetworkProcessor(RNSNetworkExecutor); + } + + // Note: potentially create persistent server_identity (utility rnid) and load it from file + public void start() throws IOException, DataException { + + // create identity either from file or new (creating new keys) + var serverIdentityPath = reticulum.getStoragePath().resolve(APP_NAME); + if (Files.isReadable(serverIdentityPath)) { + server_identity = Identity.fromFile(serverIdentityPath); + log.info("server identity loaded from file {}", serverIdentityPath.toString()); + } else { + server_identity = new Identity(); + log.info("new server identity created dynamically."); + } + log.debug("Server Identity: {}", server_identity.toString()); + + // show the ifac_size of the configured interfaces (debug code) + for (ConnectionInterface i: Transport.getInstance().getInterfaces() ) { + log.info("interface {}, length: {}", i.getInterfaceName(), i.getIfacSize()); + } + + baseDestination = new Destination( + server_identity, + Direction.IN, + DestinationType.SINGLE, + APP_NAME, + "core" + ); + //// ideas for other entry points + //dataDestination = new Destination( + // server_identity, + // Direction.IN, + // DestinationType.SINGLE, + // APP_NAME, + // "core", + // "qdn" + //); + //liveDestination = new Destination( + // server_identity, + // Direction.IN, + // DestinationType.SINGLE, + // APP_NAME, + // "core", + // "live" + //); + log.info("Destination "+Hex.encodeHexString(baseDestination.getHash())+" "+baseDestination.getName()+" running."); + //log.info("Destination "+Hex.encodeHexString(dataDestination.getHash())+" "+dataDestination.getName()+" running."); + + baseDestination.setProofStrategy(ProofStrategy.PROVE_ALL); + //dataDestination.setProofStrategy(ProofStrategy.PROVE_ALL); + + baseDestination.setAcceptLinkRequests(true); + //dataDestination.setAcceptLinkRequests(true); + //baseDestination.setLinkEstablishedCallback(this::linkExtabishedCallback); + baseDestination.setPacketCallback(this::packetCallback); + //baseDestination.setPacketCallback((message, packet) -> { + // log.info("xyz - Message raw {}", message); + // log.info("xyz - Packet {}", packet.toString()); + //}); + + Transport.getInstance().registerAnnounceHandler(new QAnnounceHandler()); + log.info("announceHandlers: {}", Transport.getInstance().getAnnounceHandlers()); + + baseDestination.announce(); + //dataDestination.announce(); + log.info("Sent initial announce from {} ({})", Hex.encodeHexString(baseDestination.getHash()), baseDestination.getName()); + + // Start up first networking thread (the "server loop") + //rnsNetworkEPC.start(); + } + + public void shutdown() { + isShuttingDown = true; + log.info("shutting down Reticulum"); + + // Stop processing threads (the "server loop") + //try { + // if (!this.rnsNetworkEPC.shutdown(5000)) { + // logger.warn("Network threads failed to terminate"); + // } + //} catch (InterruptedException e) { + // logger.warn("Interrupted while waiting for networking threads to terminate"); + //} + + // Disconnect peers and terminate Reticulum + for (RNSPeer p : linkedPeers) { + if (nonNull(p.getLink())) { + p.getLink().teardown(); + } + } + reticulum.exitHandler(); + } + + private void initConfig(String configDir) throws IOException { + File configDir1 = new File(defaultConfigPath); + if (!configDir1.exists()) { + configDir1.mkdir(); + } + var configPath = Path.of(configDir1.getAbsolutePath()); + Path configFile = configPath.resolve(CONFIG_FILE_NAME); + + if (Files.notExists(configFile)) { + var defaultConfig = this.getClass().getClassLoader().getResourceAsStream("reticulum_default_config.yml"); + Files.copy(defaultConfig, configFile, StandardCopyOption.REPLACE_EXISTING); + } + } + + private void packetCallback(byte[] message, Packet packet) { + log.info("xyz - Message raw {}", message); + log.info("xyz - Packet {}", packet.toString()); + } + + //public void announceBaseDestination () { + // getBaseDestination().announce(); + //} + + //public Consumer clientConnected(Link link) { + // log.info("Client connected"); + // link.setLinkClosedCallback(clientDisconnected(link)); + // link.setPacketCallback(null); + //} + + //public void clientDisconnected(Link link) { + // log.info("Client disconnected"); + // linkedPeers.remove(link); + //} + + // client part + //@Slf4j + private static class QAnnounceHandler implements AnnounceHandler { + @Override + public String getAspectFilter() { + // handle all announces + return null; + } + + @Override + @Synchronized + public void receivedAnnounce(byte[] destinationHash, Identity announcedIdentity, byte[] appData) { + var peerExists = false; + + log.info("Received an announce from {}", Hex.encodeHexString(destinationHash)); + //log.info("aspect: {}", getAspectFilter()); + //log.info("destinationhash: {}, announcedIdentity: {}, appData: {}", destinationHash, announcedIdentity, appData); + + if (nonNull(appData)) { + log.debug("The announce contained the following app data: {}", new String(appData, UTF_8)); + } + + // add to peer list if we can use more peers + //synchronized (this) { + List lps = RNSNetwork.getInstance().getLinkedPeers(); + if (lps.size() < MAX_PEERS) { + for (RNSPeer p : lps) { + //log.info("peer exists: hash: {}, destinationHash: {}", p.getDestinationLink().getDestination().getHash(), destinationHash); + if (Arrays.equals(p.getDestinationLink().getDestination().getHash(), destinationHash)) { + peerExists = true; + log.debug("peer exists: hash: {}, destinationHash: {}", p.getDestinationLink().getDestination().getHash(), destinationHash); + break; + } + } + if (!peerExists) { + //log.info("announce handler - cerate new peer: **announcedIdentity**: {}, **recall**: {}", announcedIdentity, recall(destinationHash)); + RNSPeer newPeer = new RNSPeer(destinationHash); + lps.add(newPeer); + log.info("added new RNSPeer, Destination - {}, Link: {}", newPeer.getDestinationHash(), newPeer.getDestinationLink()); + } + } + //} + } + } + + // Main thread + + //class RNSNetworkProcessor extends ExecuteProduceConsume { + // + // //private final Logger logger = LoggerFactory.getLogger(RNSNetworkProcessor.class); + // + // private final AtomicLong nextConnectTaskTimestamp = new AtomicLong(0L); // ms - try first connect once NTP syncs + // private final AtomicLong nextBroadcastTimestamp = new AtomicLong(0L); // ms - try first broadcast once NTP syncs + // + // private Iterator channelIterator = null; + // + // RNSNetworkProcessor(ExecutorService executor) { + // super(executor); + // } + // + // @Override + // protected void onSpawnFailure() { + // // For debugging: + // // ExecutorDumper.dump(this.executor, 3, ExecuteProduceConsume.class); + // } + // + // @Override + // protected Task produceTask(boolean canBlock) throws InterruptedException { + // Task task; + // + // //task = maybeProducePeerMessageTask(); + // //if (task != null) { + // // return task; + // //} + // // + // //final Long now = NTP.getTime(); + // // + // //task = maybeProducePeerPingTask(now); + // //if (task != null) { + // // return task; + // //} + // // + // //task = maybeProduceConnectPeerTask(now); + // //if (task != null) { + // // return task; + // //} + // // + // //task = maybeProduceBroadcastTask(now); + // //if (task != null) { + // // return task; + // //} + // // + // // Only this method can block to reduce CPU spin + // //return maybeProduceChannelTask(canBlock); + // + // // TODO: flesh out the tasks handled by Reticulum + // return null; + // } + // //...TODO: implement abstract methods... + //} + + + // getter / setter + private static class SingletonContainer { + private static final RNSNetwork INSTANCE = new RNSNetwork(); + } + + public static RNSNetwork getInstance() { + return SingletonContainer.INSTANCE; + } + + public List getLinkedPeers() { + synchronized(this.linkedPeers) { + //return new ArrayList<>(this.linkedPeers); + return this.linkedPeers; + } + } + + public Integer getTotalPeers() { + synchronized (this) { + return linkedPeers.size(); + } + } + + public Destination getBaseDestination() { + return baseDestination; + } + + // maintenance + + //private static class AnnounceTimer { + // //public void main(String[] args) throws InterruptedException + // public void main(String[] args) throws InterruptedException + // { + // Timer timer = new Timer(); + // // run timer every 10s (10000ms) + // timer.schedule(new TimerTask() { + // @Override + // public void run() { + // System.out.println("AnnounceTimer: " + new java.util.Date()); + // } + // }, 0, 10000); + // } + //} + + @Synchronized + public void prunePeers() throws DataException { + // run periodically (by the Controller) + //log.info("Peer list (linkedPeers): {}",this.linkedPeers.toString()); + //synchronized(this) { + //List linkList = getLinkedPeers(); + List peerList = this.linkedPeers; + log.info("List of RNSPeers: {}", this.linkedPeers); + //log.info("number of links (linkedPeers) before prunig: {}", this.linkedPeers.size()); + Link pLink; + LinkStatus lStatus; + for (RNSPeer p: peerList) { + pLink = p.getLink(); + lStatus = pLink.getStatus(); + //log.debug("link status: "+lStatus.toString()); + // lStatus in: PENDING, HANDSHAKE, ACTIVE, STALE, CLOSED + if (lStatus == CLOSED) { + p.resetPeer(); + peerList.remove(p); + } else if (lStatus == STALE) { + pLink.teardown(); + p.resetPeer(); + peerList.remove(p); + } else if (lStatus == PENDING) { + log.info("prunePeers - link state still {}", lStatus); + // TODO: can we help the Link along somehow? + } + } + log.info("number of links (linkedPeers) after prunig: {}", this.linkedPeers.size()); + //} + maybeAnnounce(getBaseDestination()); + } + + public void maybeAnnounce(Destination d) { + if (getLinkedPeers().size() < MIN_DESIRED_PEERS) { + d.announce(); + } + } + +} + diff --git a/src/main/java/org/qortal/network/RNSPeer.java b/src/main/java/org/qortal/network/RNSPeer.java new file mode 100644 index 00000000..871bb347 --- /dev/null +++ b/src/main/java/org/qortal/network/RNSPeer.java @@ -0,0 +1,110 @@ +package org.qortal.network; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Objects.isNull; + +import org.qortal.network.RNSNetwork; +import io.reticulum.link.Link; +import io.reticulum.packet.Packet; +import io.reticulum.identity.Identity; +import io.reticulum.channel.Channel; +import io.reticulum.destination.Destination; +import io.reticulum.destination.DestinationType; +import io.reticulum.destination.Direction; + +import static io.reticulum.identity.IdentityKnownDestination.recall; +//import static io.reticulum.identity.IdentityKnownDestination.recallAppData; +import lombok.extern.slf4j.Slf4j; +import lombok.Setter; +import lombok.Data; +import lombok.AccessLevel; + +@Data +@Slf4j +public class RNSPeer { + + private byte[] destinationHash; + private Link destinationLink; + private Identity destinationIdentity; + @Setter(AccessLevel.PACKAGE) private long creationTimestamp; + private Long lastAccessTimestamp; + + // constructors + public RNSPeer (byte[] dhash) { + this.destinationHash = dhash; + this.destinationIdentity = recall(dhash); + Link newLink = new Link( + new Destination( + this.destinationIdentity, + Direction.OUT, + DestinationType.SINGLE, + RNSNetwork.APP_NAME, + "core" + ) + ); + this.destinationLink = newLink; + destinationLink.setPacketCallback(this::packetCallback); + } + + public RNSPeer (Link newLink) { + this.destinationHash = newLink.getDestination().getHash(); + this.destinationLink = newLink; + this.destinationIdentity = newLink.getRemoteIdentity(); + setCreationTimestamp(System.currentTimeMillis()); + this.lastAccessTimestamp = null; + destinationLink.setPacketCallback(this::packetCallback); + } + + public RNSPeer () { + this.destinationHash = null; + this.destinationLink = null; + this.destinationIdentity = null; + setCreationTimestamp(System.currentTimeMillis()); + this.lastAccessTimestamp = null; + } + + // utilities (change Link type, call tasks, ...) + //... + + private void packetCallback(byte[] message, Packet packet) { + log.debug("Message raw {}", message); + log.debug("Packet {}", packet.toString()); + // ... + } + + public Link getLink() { + if (isNull(getDestinationLink())) { + Link newLink = new Link( + new Destination( + this.destinationIdentity, + Direction.OUT, + DestinationType.SINGLE, + RNSNetwork.APP_NAME, + "core" + ) + ); + this.destinationLink = newLink; + return newLink; + } + return getDestinationLink(); + } + + public Channel getChannel() { + if (isNull(getDestinationLink())) { + log.warn("link is null."); + return null; + } + setLastAccessTimestamp(System.currentTimeMillis()); + return getDestinationLink().getChannel(); + } + + public void resetPeer () { + this.destinationHash = null; + this.destinationLink = null; + this.destinationIdentity = null; + this.lastAccessTimestamp = null; + } + +} diff --git a/src/main/java/org/qortal/repository/TransactionRepository.java b/src/main/java/org/qortal/repository/TransactionRepository.java index e007586e..d4517485 100644 --- a/src/main/java/org/qortal/repository/TransactionRepository.java +++ b/src/main/java/org/qortal/repository/TransactionRepository.java @@ -205,6 +205,15 @@ public interface TransactionRepository { */ public List getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException; + /** + * Returns list of transfer asset transaction creators. + * This uses confirmed transactions only. + * + * @return + * @throws DataException + */ + public List getConfirmedTransferAssetCreators() throws DataException; + /** * Returns list of transactions pending approval, with optional txGgroupId filtering. *

diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 829f7aab..54af22e9 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -1047,6 +1047,11 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE INDEX ArbitraryIdentifierIndex ON ArbitraryTransactions (identifier)"); break; + case 49: + // Update blocks minted penalty + stmt.execute("UPDATE Accounts SET blocks_minted_penalty = -5000000 WHERE blocks_minted_penalty < 0"); + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 5b41a85d..fe0b4d0b 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -1043,6 +1043,33 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + public List getConfirmedTransferAssetCreators() throws DataException { + List transferAssetCreators = new ArrayList<>(); + + String sql = "SELECT account " + + "FROM TransferAssetTransactions " + + "JOIN Accounts ON Accounts.public_key = TransferAssetTransactions.sender " + + "JOIN Transactions ON Transactions.signature = TransferAssetTransactions.signature " + + "WHERE block_height IS NOT NULL AND TransferAssetTransactions.recipient != Accounts.account " + + "GROUP BY account " + + "ORDER BY account"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { + if (resultSet == null) + return transferAssetCreators; + + do { + String address = resultSet.getString(1); + + transferAssetCreators.add(address); + } while (resultSet.next()); + + return transferAssetCreators; + } catch (SQLException e) { + throw new DataException("Unable to fetch transfer asset from repository", e); + } + } + @Override public List getApprovalPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index ba90208b..df59c037 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -62,16 +62,6 @@ public class Settings { private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses private String bindAddressFallback = "0.0.0.0"; // Some systems are unable to bind using IPv6 - // UI servers - private int uiPort = 12388; - private String[] uiLocalServers = new String[] { - "localhost", "127.0.0.1" - }; - private String[] uiRemoteServers = new String[] { - "node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org", - "node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org" - }; - // API-related private boolean apiEnabled = true; private Integer apiPort; @@ -138,11 +128,11 @@ public class Settings { private long repositoryCheckpointInterval = 60 * 60 * 1000L; // 1 hour (ms) default /** Whether to show a notification when we perform repository 'checkpoint'. */ private boolean showCheckpointNotification = false; - /* How many blocks to cache locally. Defaulted to 10, which covers a typical Synchronizer request + a few spare */ - private int blockCacheSize = 10; + /* How many blocks to cache locally. Defaulted to 10, which covers a typical Synchronizer request + a few spare - increased to 100 */ + private int blockCacheSize = 100; /** Maximum number of transactions for the block minter to include in a block */ - private int maxTransactionsPerBlock = 50; + private int maxTransactionsPerBlock = 100; /** How long to keep old, full, AT state data (ms). */ private long atStatesMaxLifetime = 5 * 24 * 60 * 60 * 1000L; // milliseconds @@ -164,7 +154,7 @@ public class Settings { private boolean lite = false; /** Whether we should prune old data to reduce database size - * This prevents the node from being able to serve older blocks */ + * This prevents the node from being able to serve older blocks - No longer used */ private boolean topOnly = false; /** The amount of recent blocks we should keep when pruning */ private int pruneBlockLimit = 6000; @@ -205,13 +195,13 @@ public class Settings { /** Minimum number of peers to allow block minting / synchronization. */ private int minBlockchainPeers = 3; /** Target number of outbound connections to peers we should make. */ - private int minOutboundPeers = 16; + private int minOutboundPeers = 32; /** Maximum number of peer connections we allow. */ - private int maxPeers = 40; + private int maxPeers = 60; /** Number of slots to reserve for short-lived QDN data transfers */ - private int maxDataPeers = 4; + private int maxDataPeers = 5; /** Maximum number of threads for network engine. */ - private int maxNetworkThreadPoolSize = 120; + private int maxNetworkThreadPoolSize = 620; /** Maximum number of threads for network proof-of-work compute, used during handshaking. */ private int networkPoWComputePoolSize = 2; /** Maximum number of retry attempts if a peer fails to respond with the requested data */ @@ -221,7 +211,7 @@ public class Settings { public long recoveryModeTimeout = 9999999999999L; /** Minimum peer version number required in order to sync with them */ - private String minPeerVersion = "4.3.2"; + private String minPeerVersion = "4.5.0"; /** Whether to allow connections with peers below minPeerVersion * If true, we won't sync with them but they can still sync with us, and will show in the peers list * If false, sync will be blocked both ways, and they will not appear in the peers list */ @@ -269,7 +259,7 @@ public class Settings { /** Repository storage path. */ private String repositoryPath = "db"; /** Repository connection pool size. Needs to be a bit bigger than maxNetworkThreadPoolSize */ - private int repositoryConnectionPoolSize = 240; + private int repositoryConnectionPoolSize = 1920; private List fixedNetwork; // Export/import @@ -282,8 +272,7 @@ public class Settings { private String[] bootstrapHosts = new String[] { "http://bootstrap.qortal.org", "http://bootstrap2.qortal.org", - "http://bootstrap3.qortal.org", - "http://bootstrap.qortal.online" + "http://bootstrap3.qortal.org" }; // Auto-update sources @@ -370,7 +359,7 @@ public class Settings { /** Whether to allow public (decryptable) data to be stored */ private boolean publicDataEnabled = true; /** Whether to allow private (non-decryptable) data to be stored */ - private boolean privateDataEnabled = false; + private boolean privateDataEnabled = true; /** Maximum total size of hosted data, in bytes. Unlimited if null */ private Long maxStorageCapacity = null; @@ -620,18 +609,6 @@ public class Settings { return this.localeLang; } - public int getUiServerPort() { - return this.uiPort; - } - - public String[] getLocalUiServers() { - return this.uiLocalServers; - } - - public String[] getRemoteUiServers() { - return this.uiRemoteServers; - } - public boolean isApiEnabled() { return this.apiEnabled; } diff --git a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java index 79976199..a89e60c0 100644 --- a/src/main/java/org/qortal/transaction/RegisterNameTransaction.java +++ b/src/main/java/org/qortal/transaction/RegisterNameTransaction.java @@ -54,6 +54,10 @@ public class RegisterNameTransaction extends Transaction { Account registrant = getRegistrant(); String name = this.registerNameTransactionData.getName(); + int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight(); + final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 1180; + final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height(); + // Check name size bounds int nameLength = Utf8.encodedLength(name); if (nameLength < Name.MIN_NAME_SIZE || nameLength > Name.MAX_NAME_SIZE) @@ -76,6 +80,10 @@ public class RegisterNameTransaction extends Transaction { if (registrant.getConfirmedBalance(Asset.QORT) < this.registerNameTransactionData.getFee()) return ValidationResult.NO_BALANCE; + // Check if we are on algo runs + if (blockchainHeight >= start && blockchainHeight <= end) + return ValidationResult.TEMPORARY_DISABLED; + return ValidationResult.OK; } diff --git a/src/main/java/org/qortal/transaction/RewardShareTransaction.java b/src/main/java/org/qortal/transaction/RewardShareTransaction.java index ab66dec6..635c8c8a 100644 --- a/src/main/java/org/qortal/transaction/RewardShareTransaction.java +++ b/src/main/java/org/qortal/transaction/RewardShareTransaction.java @@ -3,6 +3,7 @@ package org.qortal.transaction; import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; import org.qortal.asset.Asset; +import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.account.RewardShareData; @@ -180,6 +181,33 @@ public class RewardShareTransaction extends Transaction { // Nothing to do } + @Override + public boolean isConfirmableAtHeight(int height) { + final int startV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() - 15; + final int startV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height() - 15; + final int endV2 = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height() + 10; + final int endV3 = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height() + 10; + + if (height >= BlockChain.getInstance().getUnconfirmableRewardSharesHeight()) { + // Not confirmable in online accounts or distribution blocks + if (Block.isOnlineAccountsBlock(height) || Block.isBatchRewardDistributionBlock(height)) { + return false; + } + } + + if (height >= startV2 && height <= endV2) { + // Not confirmable on algo V2 run + return false; + } + + if (height >= startV3 && height <= endV3) { + // Not confirmable on algo V3 run + return false; + } + + return true; + } + @Override public void process() throws DataException { PublicKeyAccount mintingAccount = getMintingAccount(); diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index 61b78ade..5e5b9fba 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -173,6 +173,7 @@ public abstract class Transaction { INVALID_OPTION_LENGTH(20), DUPLICATE_OPTION(21), POLL_ALREADY_EXISTS(22), + POLL_ALREADY_HAS_VOTES(23), POLL_DOES_NOT_EXIST(24), POLL_OPTION_DOES_NOT_EXIST(25), ALREADY_VOTED_FOR_THAT_OPTION(26), @@ -246,6 +247,8 @@ public abstract class Transaction { NAME_BLOCKED(97), GROUP_APPROVAL_REQUIRED(98), ACCOUNT_NOT_TRANSFERABLE(99), + TRANSFER_PRIVS_DISABLED(100), + TEMPORARY_DISABLED(101), INVALID_BUT_OK(999), NOT_YET_RELEASED(1000), NOT_SUPPORTED(1001); @@ -904,6 +907,15 @@ public abstract class Transaction { return true; } + /** + * Returns whether transaction is confirmable in a block at a given height. + * @return + */ + public boolean isConfirmableAtHeight(int height) { + /* To be optionally overridden */ + return true; + } + /** * Returns whether transaction can be added to the blockchain. *

diff --git a/src/main/java/org/qortal/transaction/TransferAssetTransaction.java b/src/main/java/org/qortal/transaction/TransferAssetTransaction.java index 50c6f24a..2144e4b9 100644 --- a/src/main/java/org/qortal/transaction/TransferAssetTransaction.java +++ b/src/main/java/org/qortal/transaction/TransferAssetTransaction.java @@ -1,6 +1,7 @@ package org.qortal.transaction; import org.qortal.account.Account; +import org.qortal.block.BlockChain; import org.qortal.data.PaymentData; import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransferAssetTransactionData; @@ -51,6 +52,14 @@ public class TransferAssetTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { + int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight(); + final int start = BlockChain.getInstance().getSelfSponsorshipAlgoV2Height(); + final int end = BlockChain.getInstance().getSelfSponsorshipAlgoV3Height(); + + // Check if we are on algo runs + if (blockchainHeight >= start && blockchainHeight <= end) + return ValidationResult.ASSET_NOT_SPENDABLE; + // Wrap asset transfer as a payment and delegate final payment checks to Payment class return new Payment(this.repository).isValid(this.transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), this.transferAssetTransactionData.getFee()); } diff --git a/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java b/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java index de3038d8..588f0d09 100644 --- a/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java +++ b/src/main/java/org/qortal/transaction/TransferPrivsTransaction.java @@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.account.Account; import org.qortal.asset.Asset; +import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.account.AccountData; @@ -72,6 +73,13 @@ public class TransferPrivsTransaction extends Transaction { if (senderAccountData == null || senderAccountData.getBlocksMintedPenalty() != 0) return ValidationResult.ACCOUNT_NOT_TRANSFERABLE; + // Disable Transfer Privs (start - end) from feature trigger + long transactionTimestamp = this.transferPrivsTransactionData.getTimestamp(); + final long startTimestamp = BlockChain.getInstance().getDisableTransferPrivsTimestamp(); + final long endTimestamp = BlockChain.getInstance().getEnableTransferPrivsTimestamp(); + if (transactionTimestamp > startTimestamp && transactionTimestamp < endTimestamp) + return ValidationResult.TRANSFER_PRIVS_DISABLED; + return ValidationResult.OK; } @@ -80,6 +88,17 @@ public class TransferPrivsTransaction extends Transaction { // Nothing to do } + @Override + public boolean isConfirmableAtHeight(int height) { + if (height >= BlockChain.getInstance().getUnconfirmableRewardSharesHeight()) { + // Not confirmable in online accounts or distribution blocks + if (Block.isOnlineAccountsBlock(height) || Block.isBatchRewardDistributionBlock(height)) { + return false; + } + } + return true; + } + @Override public void process() throws DataException { Account sender = this.getSender(); diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 2fc69347..8b8373a8 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -30,6 +30,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 1659801600000, "selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000, + "selfSponsorshipAlgoV2SnapshotTimestamp": 1708360200000, + "selfSponsorshipAlgoV3SnapshotTimestamp": 1708432200000, + "referenceTimestampBlock": 1670684455220, "mempowTransactionUpdatesTimestamp": 1693558800000, "blockRewardBatchStartHeight": 1508000, "blockRewardBatchSize": 1000, @@ -93,9 +96,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 1092000, "selfSponsorshipAlgoV1Height": 1092400, + "selfSponsorshipAlgoV2Height": 1611200, + "selfSponsorshipAlgoV3Height": 1612200, "feeValidationFixTimestamp": 1671918000000, "chatReferenceTimestamp": 1674316800000, - "arbitraryOptionalFeeTimestamp": 1680278400000 + "arbitraryOptionalFeeTimestamp": 1680278400000, + "unconfirmableRewardSharesHeight": 1575500, + "disableTransferPrivsTimestamp": 1706745000000, + "enableTransferPrivsTimestamp": 1709251200000 }, "checkpoints": [ { "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" } diff --git a/src/main/resources/i18n/ApiError_he.properties b/src/main/resources/i18n/ApiError_he.properties new file mode 100644 index 00000000..5ce597f4 --- /dev/null +++ b/src/main/resources/i18n/ApiError_he.properties @@ -0,0 +1,83 @@ +#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) +# Keys are from api.ApiError enum + +# "localeLang": "he", + +### 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 = מזהה נכס ×œ× ×—×•×§×™ + +INVALID_ORDER_ID = מזהה הזמנת נכס ×œ× ×—×•×§×™ + +ORDER_UNKNOWN = מזהה הזמנת נכס ×œ× ×™×“×•×¢ + +### 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 = עמית ×œ× ×”×©×™×‘ בזמן המותר diff --git a/src/main/resources/i18n/SysTray_de.properties b/src/main/resources/i18n/SysTray_de.properties index c92130f1..c6815879 100644 --- a/src/main/resources/i18n/SysTray_de.properties +++ b/src/main/resources/i18n/SysTray_de.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = Münzprägung inaktiv MINTING_ENABLED = \u2714 Münzprägung aktiv -OPEN_UI = Öffne Benutzeroberfläche - PERFORMING_DB_CHECKPOINT = Speichere unerfasste Datenbankänderungen... PERFORMING_DB_MAINTENANCE = Planmäßige Wartung wird durchgeführt... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronisiere Uhrzeit SYNCHRONIZING_BLOCKCHAIN = Synchronisiere SYNCHRONIZING_CLOCK = Uhrzeit wird synchronisiert + +RESTARTING_NODE = Knoten wird neu gestartet + +APPLYING_RESTARTING_NODE = Neustart des knotens wird angewendet. Bitte haben Sie Geduld... + +BOOTSTRAP_NODE = Bootstrapping-Knoten + +APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap anwenden und Knoten neu starten. Bitte haben Sie Geduld... diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties index 39940be0..302cc8d3 100644 --- a/src/main/resources/i18n/SysTray_en.properties +++ b/src/main/resources/i18n/SysTray_en.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = NOT minting MINTING_ENABLED = \u2714 Minting -OPEN_UI = Open UI - PERFORMING_DB_CHECKPOINT = Saving uncommitted database changes... PERFORMING_DB_MAINTENANCE = Performing scheduled maintenance... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronize clock SYNCHRONIZING_BLOCKCHAIN = Synchronizing SYNCHRONIZING_CLOCK = Synchronizing clock + +RESTARTING_NODE = Restarting Node + +APPLYING_RESTARTING_NODE = Applying restarting node. Please be patient... + +BOOTSTRAP_NODE = Bootstrapping Node + +APPLYING_BOOTSTRAP_AND_RESTARTING = Applying bootstrap and restarting node. Please be patient... diff --git a/src/main/resources/i18n/SysTray_es.properties b/src/main/resources/i18n/SysTray_es.properties index 36cbb22c..8f2eec7d 100644 --- a/src/main/resources/i18n/SysTray_es.properties +++ b/src/main/resources/i18n/SysTray_es.properties @@ -33,8 +33,6 @@ MINTING_DISABLED = Acuñación NO habilitada MINTING_ENABLED = \u2714 Acuñación habilitada -OPEN_UI = IU abierta - PERFORMING_DB_CHECKPOINT = Guardando cambios de base de datos no confirmados... PERFORMING_DB_MAINTENANCE = Realizando mantenimiento programado... @@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Sincronizar reloj SYNCHRONIZING_BLOCKCHAIN = Sincronizando SYNCHRONIZING_CLOCK = Sincronizando reloj + +RESTARTING_NODE = Reiniciando el nodo + +APPLYING_RESTARTING_NODE = Aplicando el nodo de reinicio. Por favor sea paciente... + +BOOTSTRAP_NODE = Nodo de arranque + +APPLYING_BOOTSTRAP_AND_RESTARTING = Aplicando bootstrap y reiniciando el nodo. Por favor sea paciente... diff --git a/src/main/resources/i18n/SysTray_fi.properties b/src/main/resources/i18n/SysTray_fi.properties index 4038d615..8c810880 100644 --- a/src/main/resources/i18n/SysTray_fi.properties +++ b/src/main/resources/i18n/SysTray_fi.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = EI lyö rahaa MINTING_ENABLED = \u2714 Lyö rahaa -OPEN_UI = Avaa UI - PERFORMING_DB_CHECKPOINT = Tallentaa kommittoidut tietokantamuutokset... PERFORMING_DB_MAINTENANCE = Suoritetaan määräaikaishuoltoa... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synkronisoi kello SYNCHRONIZING_BLOCKCHAIN = Synkronisoi SYNCHRONIZING_CLOCK = Synkronisoi kelloa + +RESTARTING_NODE = Käynnistetään uudelleen solmu + +APPLYING_RESTARTING_NODE = Käytetään uudelleenkäynnistyssolmua. Olkaa kärsivällisiä... + +BOOTSTRAP_NODE = Käynnistyssolmu + +APPLYING_BOOTSTRAP_AND_RESTARTING = Käynnistetään ja käynnistetään solmu uudelleen. Olkaa kärsivällisiä... diff --git a/src/main/resources/i18n/SysTray_fr.properties b/src/main/resources/i18n/SysTray_fr.properties index 2e376842..2ab71c72 100644 --- a/src/main/resources/i18n/SysTray_fr.properties +++ b/src/main/resources/i18n/SysTray_fr.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = NE mint PAS MINTING_ENABLED = \u2714 Minting -OPEN_UI = Ouvrir l'interface - PERFORMING_DB_CHECKPOINT = Enregistrement des modifications de base de données non validées... PERFORMING_DB_MAINTENANCE = Entrain d'effectuer la maintenance programmée... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Mettre l'heure à jour SYNCHRONIZING_BLOCKCHAIN = Synchronisation SYNCHRONIZING_CLOCK = Synchronisation de l'heure + +RESTARTING_NODE = Redémarrage du nÅ“ud + +APPLYING_RESTARTING_NODE = Application du redémarrage du nÅ“ud. S'il vous plaît, soyez patient... + +BOOTSTRAP_NODE = NÅ“ud d'amorçage + +APPLYING_BOOTSTRAP_AND_RESTARTING = Application du bootstrap et redémarrage du nÅ“ud. S'il vous plaît, soyez patient... diff --git a/src/main/resources/i18n/SysTray_he.properties b/src/main/resources/i18n/SysTray_he.properties new file mode 100644 index 00000000..6de7b7cc --- /dev/null +++ b/src/main/resources/i18n/SysTray_he.properties @@ -0,0 +1,54 @@ +#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) +# SysTray pop-up menu + +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 = Lite Node + +MINTING_DISABLED = כרייה מבוטלת + +MINTING_ENABLED = \u2714 הטבעה + +PERFORMING_DB_CHECKPOINT = שומר ×©×™× ×•×™×™× ×œ× ×ž×—×•×™×‘×™× ×‘×ž×¡×“ הנתוני×... + +PERFORMING_DB_MAINTENANCE = מבצע תחזוקה מתוזמנת... + +SYNCHRONIZE_CLOCK = סנכרן שעון + +SYNCHRONIZING_BLOCKCHAIN ​​= מסנכרן + +SYNCHRONIZING_CLOCK = מסנכרן שעון + +RESTARTING_NODE = הפעלה מחדש של צומת + +APPLYING_RESTARTING_NODE = החלת צומת הפעלה מחדש. ×× × ×”×ª×זר בסבלנות... + +BOOTSTRAP_NODE = צומת ×תחול + +APPLYING_BOOTSTRAP_AND_RESTARTING = החלת ×תחול והפעלת צומת diff --git a/src/main/resources/i18n/SysTray_hu.properties b/src/main/resources/i18n/SysTray_hu.properties index 74ab21ac..da3a7209 100644 --- a/src/main/resources/i18n/SysTray_hu.properties +++ b/src/main/resources/i18n/SysTray_hu.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = QORT-érmeverés jelenleg nincs folyamatban MINTING_ENABLED = \u2714 QORT-érmeverés folyamatban -OPEN_UI = Felhasználói felület megnyitása - PERFORMING_DB_CHECKPOINT = Mentetlen adatbázis-módosítások mentése... PERFORMING_DB_MAINTENANCE = Ãœtemezett karbantartás... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Óra-szinkronizálás megkezdése SYNCHRONIZING_BLOCKCHAIN = Szinkronizálás SYNCHRONIZING_CLOCK = Óraszinkronizálás folyamatban + +RESTARTING_NODE = Csomópont újraindítása + +APPLYING_RESTARTING_NODE = Újraindító csomópont alkalmazása. Kérjük várjon... + +BOOTSTRAP_NODE = Rendszerindítási csomópont + +APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap alkalmazása és csomópont újraindítása. Kérjük várjon... diff --git a/src/main/resources/i18n/SysTray_it.properties b/src/main/resources/i18n/SysTray_it.properties index d966d825..c35feebc 100644 --- a/src/main/resources/i18n/SysTray_it.properties +++ b/src/main/resources/i18n/SysTray_it.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = Conio disabilitato MINTING_ENABLED = \u2714 Conio abilitato -OPEN_UI = Apri UI - PERFORMING_DB_CHECKPOINT = Salvataggio delle modifiche del database non salvate... PERFORMING_DB_MAINTENANCE = Manutenzione programmata dell'efficienza... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Sincronizzare l'orologio SYNCHRONIZING_BLOCKCHAIN = Sincronizzazione della blockchain SYNCHRONIZING_CLOCK = Sincronizzazione orologio + +RESTARTING_NODE = Riavvio del nodo + +APPLYING_RESTARTING_NODE = Applicazione del nodo di riavvio. Per favore sii paziente... + +BOOTSTRAP_NODE = Nodo di bootstrap + +APPLYING_BOOTSTRAP_AND_RESTARTING = Applicazione del bootstrap e riavvio del nodo. Per favore sii paziente... diff --git a/src/main/resources/i18n/SysTray_jp.properties b/src/main/resources/i18n/SysTray_jp.properties index c4cccb5b..d3cf13ad 100644 --- a/src/main/resources/i18n/SysTray_jp.properties +++ b/src/main/resources/i18n/SysTray_jp.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = ミント一時中止中 MINTING_ENABLED = \u2714 ミント -OPEN_UI = UIã‚’é–‹ã - PERFORMING_DB_CHECKPOINT = コミットã•ã‚Œã¦ã„ãªã„データベースã®å¤‰æ›´ã‚’ä¿å­˜ä¸­... PERFORMING_DB_MAINTENANCE = 定期メンテナンスを実行中... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = 時刻をåŒæœŸ SYNCHRONIZING_BLOCKCHAIN = ブロックãƒã‚§ãƒ¼ãƒ³ã‚’åŒæœŸä¸­ SYNCHRONIZING_CLOCK = 時刻をåŒæœŸä¸­ + +RESTARTING_NODE = ノードをå†èµ·å‹•ã—ã¦ã„ã¾ã™ + +APPLYING_RESTARTING_NODE = å†èµ·å‹•ãƒŽãƒ¼ãƒ‰ã‚’é©ç”¨ã—ã¦ã„ã¾ã™ã€‚ ã—ã°ã‚‰ããŠå¾…ã¡ãã ã•ã„... + +BOOTSTRAP_NODE = ブートストラップ ノード + +APPLYING_BOOTSTRAP_AND_RESTARTING = ブートストラップをé©ç”¨ã—ã€ãƒŽãƒ¼ãƒ‰ã‚’å†èµ·å‹•ã—ã¾ã™ã€‚ ã—ã°ã‚‰ããŠå¾…ã¡ãã ã•ã„... diff --git a/src/main/resources/i18n/SysTray_ko.properties b/src/main/resources/i18n/SysTray_ko.properties index dc6cb69b..5e165da3 100644 --- a/src/main/resources/i18n/SysTray_ko.properties +++ b/src/main/resources/i18n/SysTray_ko.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = ë¯¼íŒ…ì¤‘ì´ ì•„ë‹˜ MINTING_ENABLED = \u2714 민팅 -OPEN_UI = UI 열기 - PERFORMING_DB_CHECKPOINT = 커밋ë˜ì§€ ì•Šì€ ë°ì´í„°ë² ì´ìŠ¤ 변경 ë‚´ìš©ì„ ì €ìž¥í•˜ëŠ” 중... PERFORMING_DB_MAINTENANCE = ì˜ˆì•½ëœ ìœ ì§€ 관리 수행 중... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = 시간 ë™ê¸°í™” SYNCHRONIZING_BLOCKCHAIN = ë™ê¸°í™”중 SYNCHRONIZING_CLOCK = 시간 ë™ê¸°í™” + +RESTARTING_NODE = 노드 다시 시작 중 + +APPLYING_RESTARTING_NODE = 노드 ìž¬ì‹œìž‘ì„ ì ìš©í•©ë‹ˆë‹¤. 기다려주십시오... + +BOOTSTRAP_NODE = 부트스트래핑 노드 + +APPLYING_BOOTSTRAP_AND_RESTARTING = ë¶€íŠ¸ìŠ¤íŠ¸ëž©ì„ ì ìš©í•˜ê³  노드를 다시 시작합니다. 기다려주십시오... diff --git a/src/main/resources/i18n/SysTray_nl.properties b/src/main/resources/i18n/SysTray_nl.properties index 3d7de024..f42d3e96 100644 --- a/src/main/resources/i18n/SysTray_nl.properties +++ b/src/main/resources/i18n/SysTray_nl.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = Minten is uitgeschakeld MINTING_ENABLED = \u2714 Minten is actief -OPEN_UI = Open UI - PERFORMING_DB_CHECKPOINT = De database wordt bijgewerkt... PERFORMING_DB_MAINTENANCE = Bezig met gepland onderhoud... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronizeer klok SYNCHRONIZING_BLOCKCHAIN = Bezig met synchronizeren SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd + +RESTARTING_NODE = Knooppunt opnieuw starten + +APPLYING_RESTARTING_NODE = Herstartknooppunt toepassen. Wees alstublieft geduldig... + +BOOTSTRAP_NODE = Opstartknooppunt + +APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap toepassen en knooppunt opnieuw starten. Wees alstublieft geduldig... diff --git a/src/main/resources/i18n/SysTray_pl.properties b/src/main/resources/i18n/SysTray_pl.properties index 84740da0..12e49bda 100644 --- a/src/main/resources/i18n/SysTray_pl.properties +++ b/src/main/resources/i18n/SysTray_pl.properties @@ -33,8 +33,6 @@ MINTING_DISABLED = Mennica zamkniÄ™ta MINTING_ENABLED = \u2714 Mennica aktywna -OPEN_UI = Otwórz interfejs użytkownika - PERFORMING_DB_CHECKPOINT = Zapisywanie niezaksiÄ™gowanych zmian w bazie danych... PERFORMING_DB_MAINTENANCE = Performing scheduled maintenance... @@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Synchronizuj zegar SYNCHRONIZING_BLOCKCHAIN = Synchronizacja SYNCHRONIZING_CLOCK = Synchronizacja zegara + +RESTARTING_NODE = Ponowne uruchamianie wÄ™zÅ‚a + +APPLYING_RESTARTING_NODE = StosujÄ™ ponowne uruchomienie wÄ™zÅ‚a. ProszÄ™ być cierpliwym... + +BOOTSTRAP_NODE = WÄ™zeÅ‚ Å‚adowania poczÄ…tkowego + +APPLYING_BOOTSTRAP_AND_RESTARTING = Stosowanie Å‚adowania poczÄ…tkowego i ponowne uruchamianie wÄ™zÅ‚a. ProszÄ™ być cierpliwym... diff --git a/src/main/resources/i18n/SysTray_ro.properties b/src/main/resources/i18n/SysTray_ro.properties index 4130bbcb..7e87af8d 100644 --- a/src/main/resources/i18n/SysTray_ro.properties +++ b/src/main/resources/i18n/SysTray_ro.properties @@ -35,14 +35,20 @@ MINTING_DISABLED = nu produce moneda MINTING_ENABLED = \u2714 Minting -OPEN_UI = Deschidere interfata utilizator IU - PERFORMING_DB_CHECKPOINT = Salvarea modificarilor nerealizate ale bazei de date... -PERFORMING_DB_MAINTENANCE = Efectuarea intretinerii programate… +PERFORMING_DB_MAINTENANCE = Efectuarea intretinerii programate� SYNCHRONIZE_CLOCK = Sincronizare ceas SYNCHRONIZING_BLOCKCHAIN = Sincronizare SYNCHRONIZING_CLOCK = Se sincronizeaza ceasul + +RESTARTING_NODE = Repornirea nodului + +APPLYING_RESTARTING_NODE = Se aplica nodul de repornire. Te rog fii rabdator... + +BOOTSTRAP_NODE = Nod de bootstrap + +APPLYING_BOOTSTRAP_AND_RESTARTING = Se aplica bootstrap si se reporneste nodul. Te rog fii rabdator... diff --git a/src/main/resources/i18n/SysTray_ru.properties b/src/main/resources/i18n/SysTray_ru.properties index c8615f73..8c2b50eb 100644 --- a/src/main/resources/i18n/SysTray_ru.properties +++ b/src/main/resources/i18n/SysTray_ru.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = Чеканка отключена MINTING_ENABLED = \u2714 Чеканка активна -OPEN_UI = Открыть пользовательÑкий Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ - PERFORMING_DB_CHECKPOINT = Сохранение незафикÑированных изменений базы данных... PERFORMING_DB_MAINTENANCE = Выполнение планового техничеÑкого обÑлуживаниÑ... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = Синхронизировать Ð²Ñ€ÐµÐ¼Ñ SYNCHRONIZING_BLOCKCHAIN = Ð¡Ð¸Ð½Ñ…Ñ€Ð¾Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ñ†ÐµÐ¿Ð¸ SYNCHRONIZING_CLOCK = Проверка времени + +RESTARTING_NODE = ПерезапуÑк узла + +APPLYING_RESTARTING_NODE = Применение перезапуÑка узла. ПожалуйÑта, будьте терпеливы... + +BOOTSTRAP_NODE = Узел начальной загрузки + +APPLYING_BOOTSTRAP_AND_RESTARTING = Применение начальной загрузки и перезапуÑк узла. ПожалуйÑта, будьте терпеливы... diff --git a/src/main/resources/i18n/SysTray_sv.properties b/src/main/resources/i18n/SysTray_sv.properties index 96f291b5..8fcd3193 100644 --- a/src/main/resources/i18n/SysTray_sv.properties +++ b/src/main/resources/i18n/SysTray_sv.properties @@ -33,8 +33,6 @@ MINTING_DISABLED = Präglar INTE MINTING_ENABLED = \u2714 Präglar -OPEN_UI = Öppna UI - PERFORMING_DB_CHECKPOINT = Sparar oengagerade databasändringar... PERFORMING_DB_MAINTENANCE = Utför schemalagt underhÃ¥ll... @@ -44,3 +42,11 @@ SYNCHRONIZE_CLOCK = Synkronisera klockan SYNCHRONIZING_BLOCKCHAIN = Synkroniserar SYNCHRONIZING_CLOCK = Synkroniserar klockan + +RESTARTING_NODE = Mimitian deui Node + +APPLYING_RESTARTING_NODE = Nerapkeun titik ngamimitian deui. Mangga sing sabar... + +BOOTSTRAP_NODE = Bootstrapping Node + +APPLYING_BOOTSTRAP_AND_RESTARTING = Nerapkeun bootstrap sareng hurungkeun deui titik. Mangga sing sabar... diff --git a/src/main/resources/i18n/SysTray_zh_CN.properties b/src/main/resources/i18n/SysTray_zh_CN.properties index d6848a7c..4a4e7ce6 100644 --- a/src/main/resources/i18n/SysTray_zh_CN.properties +++ b/src/main/resources/i18n/SysTray_zh_CN.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = æ²¡æœ‰é“¸å¸ MINTING_ENABLED = \u2714 é“¸å¸ -OPEN_UI = å¼€å¯Qortalç•Œé¢ - PERFORMING_DB_CHECKPOINT = 正在ä¿å­˜æœªæ交的数æ®åº“修订... PERFORMING_DB_MAINTENANCE = 正在执行定期数æ®åº“维护... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = åŒæ­¥æ—¶é’Ÿ SYNCHRONIZING_BLOCKCHAIN = 正在åŒæ­¥åŒºå—链 SYNCHRONIZING_CLOCK = 正在åŒæ­¥æ—¶é’Ÿ + +RESTARTING_NODE = é‡æ–°å¯åŠ¨èŠ‚点 + +APPLYING_RESTARTING_NODE = 应用é‡æ–°å¯åŠ¨èŠ‚点。 请è€å¿ƒç­‰å¾…... + +BOOTSTRAP_NODE = 引导节点 + +APPLYING_BOOTSTRAP_AND_RESTARTING = 应用引导程åºå¹¶é‡æ–°å¯åŠ¨èŠ‚点。 请è€å¿ƒç­‰å¾…... diff --git a/src/main/resources/i18n/SysTray_zh_TW.properties b/src/main/resources/i18n/SysTray_zh_TW.properties index eabdbb63..5dc07fe2 100644 --- a/src/main/resources/i18n/SysTray_zh_TW.properties +++ b/src/main/resources/i18n/SysTray_zh_TW.properties @@ -35,8 +35,6 @@ MINTING_DISABLED = 沒有鑄幣 MINTING_ENABLED = \u2714 é‘„å¹£ -OPEN_UI = é–‹å•“Qortalç•Œé¢ - PERFORMING_DB_CHECKPOINT = 正在ä¿å­˜æœªæ交的數據庫修訂... PERFORMING_DB_MAINTENANCE = 正在執行數據庫定期維護... @@ -46,3 +44,11 @@ SYNCHRONIZE_CLOCK = åŒæ­¥æ™‚é˜ SYNCHRONIZING_BLOCKCHAIN = 正在åŒæ­¥å€å¡Šéˆ SYNCHRONIZING_CLOCK = 正在åŒæ­¥æ™‚é˜ + +RESTARTING_NODE = é‡æ–°å•Ÿå‹•ç¯€é»ž + +APPLYING_RESTARTING_NODE = 應用é‡æ–°å•Ÿå‹•ç¯€é»žã€‚ è«‹è€å¿ƒç­‰å¾…... + +BOOTSTRAP_NODE = 引導節點 + +APPLYING_BOOTSTRAP_AND_RESTARTING = 應用引導程å¼ä¸¦é‡æ–°å•Ÿå‹•ç¯€é»žã€‚ è«‹è€å¿ƒç­‰å¾…... diff --git a/src/main/resources/i18n/TransactionValidity_de.properties b/src/main/resources/i18n/TransactionValidity_de.properties index eab7fb9e..a2019670 100644 --- a/src/main/resources/i18n/TransactionValidity_de.properties +++ b/src/main/resources/i18n/TransactionValidity_de.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = Transaktion existiert bereits TRANSACTION_UNKNOWN = Transaktion unbekannt TX_GROUP_ID_MISMATCH = die Gruppen-ID der Transaktion stimmt nicht überein + +TRANSFER_PRIVS_DISABLED = Ãœbertragungsberechtigungen deaktiviert + +TEMPORARY_DISABLED = Namensregistrierung vorübergehend deaktiviert diff --git a/src/main/resources/i18n/TransactionValidity_en.properties b/src/main/resources/i18n/TransactionValidity_en.properties index 3f33771d..a93db3da 100644 --- a/src/main/resources/i18n/TransactionValidity_en.properties +++ b/src/main/resources/i18n/TransactionValidity_en.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaction already exists TRANSACTION_UNKNOWN = transaction unknown TX_GROUP_ID_MISMATCH = transaction's group ID does not match + +TRANSFER_PRIVS_DISABLED = transfer privileges disabled + +TEMPORARY_DISABLED = Name registration temporary disabled diff --git a/src/main/resources/i18n/TransactionValidity_es.properties b/src/main/resources/i18n/TransactionValidity_es.properties index 7c357009..8ac7ccf4 100644 --- a/src/main/resources/i18n/TransactionValidity_es.properties +++ b/src/main/resources/i18n/TransactionValidity_es.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transacción ya existe TRANSACTION_UNKNOWN = transacción desconocida TX_GROUP_ID_MISMATCH = el ID de grupo de la transacción no coincide + +TRANSFER_PRIVS_DISABLED = privilegios de transferencia deshabilitados + +TEMPORARY_DISABLED = Registro de nombre temporalmente deshabilitado diff --git a/src/main/resources/i18n/TransactionValidity_fi.properties b/src/main/resources/i18n/TransactionValidity_fi.properties index ec658bc1..a7bc9c0a 100644 --- a/src/main/resources/i18n/TransactionValidity_fi.properties +++ b/src/main/resources/i18n/TransactionValidity_fi.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaktio on jo olemassa TRANSACTION_UNKNOWN = tuntematon transaktio TX_GROUP_ID_MISMATCH = transaktion ryhmä-ID:n vastaavuusvirhe + +TRANSFER_PRIVS_DISABLED = siirtooikeudet poistettu käytöstä + +TEMPORARY_DISABLED = Nimen rekisteröinti tilapäisesti poistettu käytöstä diff --git a/src/main/resources/i18n/TransactionValidity_fr.properties b/src/main/resources/i18n/TransactionValidity_fr.properties index e030bc0d..55ae9082 100644 --- a/src/main/resources/i18n/TransactionValidity_fr.properties +++ b/src/main/resources/i18n/TransactionValidity_fr.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transaction existe déjà TRANSACTION_UNKNOWN = transaction inconnue TX_GROUP_ID_MISMATCH = l'identifiant du groupe de transaction ne correspond pas + +TRANSFER_PRIVS_DISABLED = privilèges de transfert désactivés + +TEMPORARY_DISABLED = Enregistrement du nom temporairement désactivé diff --git a/src/main/resources/i18n/TransactionValidity_he.properties b/src/main/resources/i18n/TransactionValidity_he.properties new file mode 100644 index 00000000..2f9338f0 --- /dev/null +++ b/src/main/resources/i18n/TransactionValidity_he.properties @@ -0,0 +1,199 @@ +# + +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 = ××™ הת×מה של מזהה הקבוצה + +GROUP_OWNER_CANNOT_LEAVE = בעל הקבוצה ×œ× ×™×›×•×œ לעזוב ×ת הקבוצה + +HAVE_EQUALS_WANT = have-asset ×–×”×” ל-want-asset + +INCORRECT_NONCE = ×œ× ×ª×§×™×Ÿ של PoW + +INSUFFICIENT_FEE = עמלה ×œ× ×ž×¡×¤×§×ª + +INVALID_ADDRESS = כתובת ×œ× ×—×•×§×™×ª + +INVALID_AMOUNT = ×¡×›×•× ×œ× ×—×•×§×™ + +INVALID_ASSET_OWNER = בעל נכס ×œ× ×—×•×§×™ + +INVALID_AT_TRANSACTION = עסקת AT ×œ× ×—×•×§×™×ª + +INVALID_AT_TYPE_LENGTH = ×ורך AT 'סוג' ×œ× ×—×•×§×™ + +INVALID_BUT_OK = ×œ× ×—×•×§×™ ×בל בסדר + +INVALID_CREATION_BYTES = ×‘×ª×™× ×œ× ×—×•×§×™×™× ×©×œ יצירה + +INVALID_DATA_LENGTH = ×ורך × ×ª×•× ×™× ×œ× ×—×•×§×™ + +INVALID_DESCRIPTION_LENGTH = ×ורך תי×ור ×œ× ×—×•×§×™ + +INVALID_GROUP_APPROVAL_THRESHOLD = סף ×œ× ×—×•×§×™ ל×ישור קבוצה + +INVALID_GROUP_BLOCK_DELAY = עיכוב חסימת ×ישור קבוצה ×œ× ×—×•×§×™ + +INVALID_GROUP_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 = מזהה קבוצת עסק×ות ×œ× ×—×•×§×™ + +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 = בסדר + +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 = מזהה הקבוצה של העסקה ×ינו תו×× + +TRANSFER_PRIVS_DISABLED = הרש×ות העברה מושבתות + +TEMPORARY_DISABLED = ×¨×™×©×•× ×©×ž×•×ª מושבת זמנית diff --git a/src/main/resources/i18n/TransactionValidity_hu.properties b/src/main/resources/i18n/TransactionValidity_hu.properties index 1b2558bb..2dbd9fd0 100644 --- a/src/main/resources/i18n/TransactionValidity_hu.properties +++ b/src/main/resources/i18n/TransactionValidity_hu.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = ez a tranzakció már létezik TRANSACTION_UNKNOWN = ismeretlen tranzakció TX_GROUP_ID_MISMATCH = a tranzakció csoportazonosítója nem egyezik + +TRANSFER_PRIVS_DISABLED = átviteli jogosultságok letiltva + +TEMPORARY_DISABLED = A névregisztráció ideiglenesen le van tiltva diff --git a/src/main/resources/i18n/TransactionValidity_it.properties b/src/main/resources/i18n/TransactionValidity_it.properties index 390f914a..a520d5ae 100644 --- a/src/main/resources/i18n/TransactionValidity_it.properties +++ b/src/main/resources/i18n/TransactionValidity_it.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = la transazione già esiste TRANSACTION_UNKNOWN = transazione sconosciuta TX_GROUP_ID_MISMATCH = identificazione di gruppo della transazione non corrisponde + +TRANSFER_PRIVS_DISABLED = privilegi di trasferimento disabilitati + +TEMPORARY_DISABLED = Registrazione del nome temporaneamente disabilitata diff --git a/src/main/resources/i18n/TransactionValidity_jp.properties b/src/main/resources/i18n/TransactionValidity_jp.properties index 9540372a..3827635c 100644 --- a/src/main/resources/i18n/TransactionValidity_jp.properties +++ b/src/main/resources/i18n/TransactionValidity_jp.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = æ—¢ã«ãƒˆãƒ©ãƒ³ã‚¶ã‚¯ã‚·ãƒ§ãƒ³ã¯å­˜åœ¨ã—ã¾ã™ TRANSACTION_UNKNOWN = ä¸æ˜Žãªãƒˆãƒ©ãƒ³ã‚¶ã‚¯ã‚·ãƒ§ãƒ³ TX_GROUP_ID_MISMATCH = トランザクションã®ã‚°ãƒ«ãƒ¼ãƒ—IDãŒä¸€è‡´ã—ã¾ã›ã‚“ + +TRANSFER_PRIVS_DISABLED = 転é€æ¨©é™ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™ + +TEMPORARY_DISABLED = åå‰ã®ç™»éŒ²ãŒä¸€æ™‚çš„ã«ç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™ diff --git a/src/main/resources/i18n/TransactionValidity_ko.properties b/src/main/resources/i18n/TransactionValidity_ko.properties index a12b33f6..2667471c 100644 --- a/src/main/resources/i18n/TransactionValidity_ko.properties +++ b/src/main/resources/i18n/TransactionValidity_ko.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = 거래가 ì´ë¯¸ 존재합니다 TRANSACTION_UNKNOWN = ì•Œ 수 없는 거래 TX_GROUP_ID_MISMATCH = íŠ¸ëžœìž­ì…˜ì˜ ê·¸ë£¹ IDê°€ ì¼ì¹˜í•˜ì§€ 않습니다 + +TRANSFER_PRIVS_DISABLED = 권한 ì´ì „ì´ ë¹„í™œì„±í™”ë˜ì—ˆìŠµë‹ˆë‹¤. + +TEMPORARY_DISABLED = ì´ë¦„ 등ë¡ì´ ì¼ì‹œì ìœ¼ë¡œ 비활성화ë˜ì—ˆìŠµë‹ˆë‹¤. diff --git a/src/main/resources/i18n/TransactionValidity_nl.properties b/src/main/resources/i18n/TransactionValidity_nl.properties index f92adf72..4f0dd8b7 100644 --- a/src/main/resources/i18n/TransactionValidity_nl.properties +++ b/src/main/resources/i18n/TransactionValidity_nl.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transactie bestaat reeds TRANSACTION_UNKNOWN = transactie onbekend TX_GROUP_ID_MISMATCH = groep-ID komt niet overeen + +TRANSFER_PRIVS_DISABLED = overdrachtsrechten uitgeschakeld + +TEMPORARY_DISABLED = Naamregistratie tijdelijk uitgeschakeld diff --git a/src/main/resources/i18n/TransactionValidity_pl.properties b/src/main/resources/i18n/TransactionValidity_pl.properties index bcdceb6e..50944674 100644 --- a/src/main/resources/i18n/TransactionValidity_pl.properties +++ b/src/main/resources/i18n/TransactionValidity_pl.properties @@ -194,3 +194,6 @@ TRANSACTION_UNKNOWN = transakcja nieznana TX_GROUP_ID_MISMATCH = niezgodność ID grupy transakcji +TRANSFER_PRIVS_DISABLED = uprawnienia do przenoszenia wyÅ‚Ä…czone + +TEMPORARY_DISABLED = Rejestracja nazwy tymczasowo wyÅ‚Ä…czona diff --git a/src/main/resources/i18n/TransactionValidity_ro.properties b/src/main/resources/i18n/TransactionValidity_ro.properties index 6c67f31b..4c149a62 100644 --- a/src/main/resources/i18n/TransactionValidity_ro.properties +++ b/src/main/resources/i18n/TransactionValidity_ro.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = tranzactia exista deja TRANSACTION_UNKNOWN = tranzactie necunoscuta TX_GROUP_ID_MISMATCH = ID-ul de grup al tranzactiei nu se potriveste + +TRANSFER_PRIVS_DISABLED = privilegii de transfer dezactivate + +TEMPORARY_DISABLED = ÃŽnregistrarea numelui a fost temporar dezactivată diff --git a/src/main/resources/i18n/TransactionValidity_ru.properties b/src/main/resources/i18n/TransactionValidity_ru.properties index 2818374a..79307d7d 100644 --- a/src/main/resources/i18n/TransactionValidity_ru.properties +++ b/src/main/resources/i18n/TransactionValidity_ru.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = Ñ‚Ñ€Ð°Ð½Ð·Ð°ÐºÑ†Ð¸Ñ ÑущеÑтвует TRANSACTION_UNKNOWN = неизвеÑÑ‚Ð½Ð°Ñ Ñ‚Ñ€Ð°Ð½Ð·Ð°ÐºÑ†Ð¸Ñ TX_GROUP_ID_MISMATCH = не ÑоответÑтвие идентификатора группы в Ñ…Ñш транзации + +TRANSFER_PRIVS_DISABLED = права на передачу отключены + +TEMPORARY_DISABLED = РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ временно отключена diff --git a/src/main/resources/i18n/TransactionValidity_sv.properties b/src/main/resources/i18n/TransactionValidity_sv.properties index a3fec831..d4688310 100644 --- a/src/main/resources/i18n/TransactionValidity_sv.properties +++ b/src/main/resources/i18n/TransactionValidity_sv.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = transaktionen finns redan TRANSACTION_UNKNOWN = okänd transaktion TX_GROUP_ID_MISMATCH = transaktionens grupp-ID matchar inte + +TRANSFER_PRIVS_DISABLED = överföringsprivilegier inaktiverade + +TEMPORARY_DISABLED = Namnregistrering tillfälligt inaktiverad diff --git a/src/main/resources/i18n/TransactionValidity_zh_CN.properties b/src/main/resources/i18n/TransactionValidity_zh_CN.properties index d2a2ec7c..cd16bf64 100644 --- a/src/main/resources/i18n/TransactionValidity_zh_CN.properties +++ b/src/main/resources/i18n/TransactionValidity_zh_CN.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = 此交易已存在 TRANSACTION_UNKNOWN = 未知的交易 TX_GROUP_ID_MISMATCH = 群组ID交易ä¸å»åˆ + +TRANSFER_PRIVS_DISABLED = 传输æƒé™å·²ç¦ç”¨ + +TEMPORARY_DISABLED = å称注册暂时ç¦ç”¨ diff --git a/src/main/resources/i18n/TransactionValidity_zh_TW.properties b/src/main/resources/i18n/TransactionValidity_zh_TW.properties index e88addb9..d039a8e7 100644 --- a/src/main/resources/i18n/TransactionValidity_zh_TW.properties +++ b/src/main/resources/i18n/TransactionValidity_zh_TW.properties @@ -193,3 +193,7 @@ TRANSACTION_ALREADY_EXISTS = 此交易已存在 TRANSACTION_UNKNOWN = 未知的交易 TX_GROUP_ID_MISMATCH = 群組ID交易ä¸å»åˆ + +TRANSFER_PRIVS_DISABLED = 傳輸權é™å·²åœç”¨ + +TEMPORARY_DISABLED = å稱註冊暫時åœç”¨ diff --git a/src/main/resources/reticulum_default_config.yml b/src/main/resources/reticulum_default_config.yml new file mode 100644 index 00000000..18e8b729 --- /dev/null +++ b/src/main/resources/reticulum_default_config.yml @@ -0,0 +1,93 @@ +--- +# You should probably edit it to include any additional, +# interfaces and settings you might need. + +# Only the most basic options are included in this default +# configuration. To see a more verbose, and much longer, +# configuration example, you can run the command: +# rnsd --exampleconfig + +reticulum: + + # If you enable Transport, your system will route traffic + # for other peers, pass announces and serve path requests. + # This should only be done for systems that are suited to + # act as transport nodes, ie. if they are stationary and + # always-on. This directive is optional and can be removed + # for brevity. + + enable_transport: false + + # By default, the first program to launch the Reticulum + # Network Stack will create a shared instance, that other + # programs can communicate with. Only the shared instance + # opens all the configured interfaces directly, and other + # local programs communicate with the shared instance over + # a local socket. This is completely transparent to the + # user, and should generally be turned on. This directive + # is optional and can be removed for brevity. + + share_instance: false + + # If you want to run multiple *different* shared instances + # on the same system, you will need to specify different + # shared instance ports for each. The defaults are given + # below, and again, these options can be left out if you + # don't need them. + + #shared_instance_port: 37428 + #instance_control_port: 37429 + shared_instance_port: 37438 + instance_control_port: 37439 + + # You can configure Reticulum to panic and forcibly close + # if an unrecoverable interface error occurs, such as the + # hardware device for an interface disappearing. This is + # an optional directive, and can be left out for brevity. + # This behaviour is disabled by default. + + panic_on_interface_error: false + + +# The interfaces section defines the physical and virtual +# interfaces Reticulum will use to communicate on. This +# section will contain examples for a variety of interface +# types. You can modify these or use them as a basis for +# your own config, or simply remove the unused ones. + +interfaces: + + # This interface enables communication with other + # link-local Reticulum nodes over UDP. It does not + # need any functional IP infrastructure like routers + # or DHCP servers, but will require that at least link- + # local IPv6 is enabled in your operating system, which + # should be enabled by default in almost any OS. See + # the Reticulum Manual for more configuration options. + #"Default Interface": + # type: AutoInterface + # enabled: true + + # This interface enables communication with a "backbone" + # server over TCP. + # Note: others may be added for redundancy + "TCP Client Interface mobilefabrik": + type: TCPClientInterface + enabled: true + target_host: phantom.mobilefabrik.com + target_port: 4242 + #network_name: qortal + + # This interface turns this Reticulum instance into a + # server other clients can connect to over TCP. + # To enable this instance to route traffic the above + # setting "enable_transport" needs to be set (to true). + # Note: this interface type is not yet supported by + # reticulum-network-stack. + #"TCP Server Interface": + # type: TCPServerInterface + # enabled: true + # listen_ip: 0.0.0.0 + # listen_port: 3434 + # #network_name: qortal + diff --git a/src/test/java/org/qortal/test/PenaltyFixTests.java b/src/test/java/org/qortal/test/PenaltyFixTests.java new file mode 100644 index 00000000..6d06f5f1 --- /dev/null +++ b/src/test/java/org/qortal/test/PenaltyFixTests.java @@ -0,0 +1,82 @@ +package org.qortal.test; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.Account; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.block.Block; +import org.qortal.controller.BlockMinter; +import org.qortal.data.transaction.PaymentTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.utils.NTP; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +public class PenaltyFixTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useSettings("test-settings-v2-penalty-fix.json"); + NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); + } + + @Test + public void testSingleSponsor() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Test account from real penalty data (pen-revert.json) + Account penaltyAccount = new Account(repository, "QLcAQpko5egwNjifueCAeAsT8CAj2Sr5qJ"); + + // Bob sends a payment to the penalty account, so that it gets a row in the Accounts table + TransactionData paymentData = new PaymentTransactionData(TestTransaction.generateBase(bobAccount), penaltyAccount.getAddress(), 1); + TransactionUtils.signAndImportValid(repository, paymentData, bobAccount); // updates paymentData's signature + + // Mint blocks up to height 4 + Block block = null; + for (int i = 2; i <= 4; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(4, (int)block.getBlockData().getHeight()); + + // Check blocks minted penalty of penalty account + assertEquals(0, (int) penaltyAccount.getBlocksMintedPenalty()); + + // Penalty revert code runs at block 5 + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(5, (int)block.getBlockData().getHeight()); + + // +5000000 blocks minted penalty should be applied + assertEquals(5000000, (int) penaltyAccount.getBlocksMintedPenalty()); + + // Orphan the last block, to simulate a re-org + BlockUtils.orphanLastBlock(repository); + + assertEquals(0, (int) penaltyAccount.getBlocksMintedPenalty()); + + // Penalty revert code runs again + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(5, (int)block.getBlockData().getHeight()); + + // Penalty should still be 5000000, rather than doubled up to 10000000 + assertEquals(5000000, (int) penaltyAccount.getBlocksMintedPenalty()); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java index 5c038de2..fe0556ca 100644 --- a/src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java +++ b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java @@ -38,7 +38,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common { @Before public void beforeTest() throws DataException { - Common.useSettings("test-settings-v2-self-sponsorship-algo.json"); + Common.useSettings("test-settings-v2-self-sponsorship-algo-v1.json"); NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); } diff --git a/src/test/java/org/qortal/test/SelfSponsorshipAlgoV2Tests.java b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV2Tests.java new file mode 100644 index 00000000..495df511 --- /dev/null +++ b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV2Tests.java @@ -0,0 +1,342 @@ +package org.qortal.test; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.Account; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.asset.Asset; +import org.qortal.block.Block; +import org.qortal.controller.BlockMinter; +import org.qortal.data.account.AccountPenaltyData; +import org.qortal.data.transaction.*; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.utils.NTP; + +import java.util.*; + +import static org.junit.Assert.*; +import static org.qortal.test.common.AccountUtils.fee; + +public class SelfSponsorshipAlgoV2Tests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useSettings("test-settings-v2-self-sponsorship-algo-v2.json"); + NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); + } + + @Test + public void tesTransferAssetsQort() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that Bob, Chloe and Dilbert are greater than level 0 + assertTrue(new Account(repository, bobAccount.getAddress()).getLevel() > 0); + assertTrue(new Account(repository, chloeAccount.getAddress()).getLevel() > 0); + assertTrue(new Account(repository, dilbertAccount.getAddress()).getLevel() > 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that Chloe and Dilbert have more than 20 qort + assertTrue(new Account(repository, chloeAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20); // 10 for transfer asset, 10 for fee + assertTrue(new Account(repository, dilbertAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20); // 10 for transfer asset, 10 for fee + + // Mint until block 10 + while (block.getBlockData().getHeight() < 10) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(10, (int) block.getBlockData().getHeight()); + + // Chloe transfer assets to Bob and Dilbert + transferAssets(repository, chloeAccount, bobAccount); + transferAssets(repository, chloeAccount, dilbertAccount); + + // Dilbert transfer assets to Bob and Chloe + transferAssets(repository, dilbertAccount, bobAccount); + transferAssets(repository, dilbertAccount, chloeAccount); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that Bob have no penalties and level 5 + assertEquals(0, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(5, (int)bobAccount.getLevel()); + + // Ensure that Chloe have no penalties and level 5 + assertEquals(0, (int) new Account(repository, chloeAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(5, (int)chloeAccount.getLevel()); + + // Ensure that Dilbert have no penalties and level6 + assertEquals(0, (int) new Account(repository, dilbertAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(6, (int)dilbertAccount.getLevel()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that Bob, Chloe and Dilbert are now have penalties + assertEquals(-5000000, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(-5000000, (int) new Account(repository, chloeAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(-5000000, (int) new Account(repository, dilbertAccount.getAddress()).getBlocksMintedPenalty()); + + // Ensure that Bob, Chloe and Dilbert are now level 0 + assertEquals(0, (int) new Account(repository, bobAccount.getAddress()).getLevel()); + assertEquals(0, (int) new Account(repository, chloeAccount.getAddress()).getLevel()); + assertEquals(0, (int) new Account(repository, dilbertAccount.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure that Bob, Chloe and Dilbert are now greater than level 0 + assertTrue(new Account(repository, bobAccount.getAddress()).getLevel() > 0); + assertTrue(new Account(repository, chloeAccount.getAddress()).getLevel() > 0); + assertTrue(new Account(repository, dilbertAccount.getAddress()).getLevel() > 0); + + // Ensure that Bob, Chloe and Dilbert have no penalties again + assertEquals(0, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, chloeAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, dilbertAccount.getAddress()).getBlocksMintedPenalty()); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testSingleTransferPrivsBeforeAlgoBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that Bob have more than 20 qort + assertTrue(new Account(repository, bobAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20); + + // Mint until block 17 (the algo runs at block 20) + while (block.getBlockData().getHeight() < 26) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(26, (int) block.getBlockData().getHeight()); + + // Bob then issues a TRANSFER_PRIVS + PrivateKeyAccount recipientAccount = randomTransferPrivs(repository, bobAccount); + + // Ensure recipient has no level (actually, no account record) at this point (pre-confirmation) + assertNull(recipientAccount.getLevel()); + + // Mint a block, so that the TRANSFER_PRIVS confirms + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Now ensure that the TRANSFER_PRIVS recipient has inherited Bob's level, and Bob is at level 0 + assertTrue(recipientAccount.getLevel() > 0); + assertEquals(0, (int)bobAccount.getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so that we can penalize Bob after transfer privs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Update blocks minted penalty for Bob + Set penalties = new LinkedHashSet<>(); + penalties.add(new AccountPenaltyData(bobAccount.getAddress(), -5000000)); + repository.getAccountRepository().updateBlocksMintedPenalties(penalties); + + // Mint a block, so that we check if Bob got penalized before algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure Bob got penalized + assertEquals(-5000000, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure recipient account has penalty too + assertEquals(-5000000, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure recipient account has no penalty again and has a level greater than 0 + assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty()); + assertTrue(new Account(repository, recipientAccount.getAddress()).getLevel() > 0); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testMultipleTransferPrivsBeforeAlgoBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that Bob, Chloe and Dilbert have more than 20 qort + assertTrue(new Account(repository, bobAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20); + assertTrue(new Account(repository, chloeAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20); + assertTrue(new Account(repository, dilbertAccount.getAddress()).getConfirmedBalance(Asset.QORT) > 20); + + // Mint until block 12 (the algo runs at block 20) + while (block.getBlockData().getHeight() < 22) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(22, (int) block.getBlockData().getHeight()); + + // Bob then issues a TRANSFER_PRIVS + PrivateKeyAccount recipientAccount1 = randomTransferPrivs(repository, bobAccount); + + // Ensure Bob's recipient has no level (actually, no account record) at this point (pre-confirmation) + assertNull(recipientAccount1.getLevel()); + + // Mint a block, so that Bob's TRANSFER_PRIVS confirms + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Now ensure that the Bob's TRANSFER_PRIVS recipient has inherited Bob's level, and Bob is at level 0 + assertTrue(recipientAccount1.getLevel() > 0); + assertEquals(0, (int)bobAccount.getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so that Chloe can issue a TRANSFER_PRIVS + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Chloe then issues a TRANSFER_PRIVS + PrivateKeyAccount recipientAccount2 = randomTransferPrivs(repository, chloeAccount); + + // Ensure Chloe's recipient has no level (actually, no account record) at this point (pre-confirmation) + assertNull(recipientAccount2.getLevel()); + + // Mint a block, so that Chloe's TRANSFER_PRIVS confirms + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Now ensure that the Chloe's TRANSFER_PRIVS recipient has inherited Chloe's level, and Chloe is at level 0 + assertTrue(recipientAccount2.getLevel() > 0); + assertEquals(0, (int)chloeAccount.getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so that Dilbert can issue a TRANSFER_PRIVS + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Dilbert then issues a TRANSFER_PRIVS + PrivateKeyAccount recipientAccount3 = randomTransferPrivs(repository, dilbertAccount); + + // Ensure Dilbert's recipient has no level (actually, no account record) at this point (pre-confirmation) + assertNull(recipientAccount3.getLevel()); + + // Mint a block, so that Dilbert's TRANSFER_PRIVS confirms + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Now ensure that the Dilbert's TRANSFER_PRIVS recipient has inherited Dilbert's level, and Dilbert is at level 0 + assertTrue(recipientAccount3.getLevel() > 0); + assertEquals(0, (int)dilbertAccount.getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount3.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so that we can penalize Bob, Chloe and Dilbert after transfer privs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Update blocks minted penalty for Bob, Chloe and Dilbert + Set penalties = new LinkedHashSet<>(); + penalties.add(new AccountPenaltyData(bobAccount.getAddress(), -5000000)); + penalties.add(new AccountPenaltyData(chloeAccount.getAddress(), -5000000)); + penalties.add(new AccountPenaltyData(dilbertAccount.getAddress(), -5000000)); + repository.getAccountRepository().updateBlocksMintedPenalties(penalties); + + // Mint a block, so that we check if Bob, Chloe and Dilbert got penalized before algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure Bob, Chloe and Dilbert got penalized + assertEquals(-5000000, (int) new Account(repository, bobAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(-5000000, (int) new Account(repository, chloeAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(-5000000, (int) new Account(repository, dilbertAccount.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure recipients accounts has penalty too + assertEquals(-5000000, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty()); + assertEquals(-5000000, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty()); + assertEquals(-5000000, (int) new Account(repository, recipientAccount3.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount3.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure recipients accounts has no penalty again and has a level greater than 0 + assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, recipientAccount3.getAddress()).getBlocksMintedPenalty()); + assertTrue(new Account(repository, recipientAccount1.getAddress()).getLevel() > 0); + assertTrue(new Account(repository, recipientAccount2.getAddress()).getLevel() > 0); + assertTrue(new Account(repository, recipientAccount3.getAddress()).getLevel() > 0); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + private static void transferAssets(Repository repository, PrivateKeyAccount senderAccount, PrivateKeyAccount recipientAccount) throws DataException { + for (int i = 0; i < 5; i++) { + // Generate new asset transfers from sender to recipient + BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), 0, senderAccount.getLastReference(), senderAccount.getPublicKey(), fee, null); + TransactionData transactionData; + transactionData = new TransferAssetTransactionData(baseTransactionData, recipientAccount.getAddress(), 1, 0); + TransactionUtils.signAndImportValid(repository, transactionData, senderAccount); // updates paymentData's signature + } + } + + private static PrivateKeyAccount randomTransferPrivs(Repository repository, PrivateKeyAccount senderAccount) throws DataException { + // Generate random recipient account + byte[] randomPrivateKey = new byte[32]; + new Random().nextBytes(randomPrivateKey); + PrivateKeyAccount recipientAccount = new PrivateKeyAccount(repository, randomPrivateKey); + + BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), 0, senderAccount.getLastReference(), senderAccount.getPublicKey(), fee, null); + TransactionData transactionData = new TransferPrivsTransactionData(baseTransactionData, recipientAccount.getAddress()); + + TransactionUtils.signAndImportValid(repository, transactionData, senderAccount); + + return recipientAccount; + } + +} \ No newline at end of file diff --git a/src/test/java/org/qortal/test/SelfSponsorshipAlgoV3Tests.java b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV3Tests.java new file mode 100644 index 00000000..bb9de4e3 --- /dev/null +++ b/src/test/java/org/qortal/test/SelfSponsorshipAlgoV3Tests.java @@ -0,0 +1,1578 @@ +package org.qortal.test; + +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.Account; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.asset.Asset; +import org.qortal.block.Block; +import org.qortal.controller.BlockMinter; +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.data.transaction.PaymentTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.data.transaction.TransferPrivsTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.test.common.AccountUtils; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.test.common.transaction.TestTransaction; +import org.qortal.transaction.Transaction; +import org.qortal.transaction.TransferPrivsTransaction; +import org.qortal.utils.NTP; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.*; +import static org.qortal.test.common.AccountUtils.fee; +import static org.qortal.transaction.Transaction.ValidationResult.ACCOUNT_NOT_TRANSFERABLE; +import static org.qortal.transaction.Transaction.ValidationResult.OK; + +public class SelfSponsorshipAlgoV3Tests extends Common { + + + @Before + public void beforeTest() throws DataException { + Common.useSettings("test-settings-v2-self-sponsorship-algo-v3.json"); + NTP.setFixedOffset(Settings.getInstance().getTestNtpOffset()); + } + + + @Test + public void testSingleSponsor() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Bob self sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(11, block.getBlockData().getOnlineAccountsCount()); + assertEquals(10, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that bob and his sponsees now have penalties + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are now level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure that bob and his sponsees are now greater than level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Ensure that bob and his sponsees have no penalties again + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testMultipleSponsors() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Chloe sponsors 10 accounts + List chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10); + List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees); + onlineAccounts.addAll(chloeSponseesOnlineAccounts); + + // Dilbert sponsors 5 accounts + List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5); + List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees); + onlineAccounts.addAll(dilbertSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(26, block.getBlockData().getOnlineAccountsCount()); + assertEquals(25, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + assertEquals(6, (int)dilbertAccount.getLevel()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that bob and his sponsees now have penalties + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that chloe and her sponsees have no penalties + List chloeAndSponsees = new ArrayList<>(chloeSponsees); + chloeAndSponsees.add(chloeAccount); + for (PrivateKeyAccount chloeSponsee : chloeAndSponsees) + assertEquals(0, (int) new Account(repository, chloeSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that dilbert and his sponsees have no penalties + List dilbertAndSponsees = new ArrayList<>(dilbertSponsees); + dilbertAndSponsees.add(dilbertAccount); + for (PrivateKeyAccount dilbertSponsee : dilbertAndSponsees) + assertEquals(0, (int) new Account(repository, dilbertSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are now level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure that bob and his sponsees are now greater than level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Ensure that bob and his sponsees have no penalties again + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that chloe and her sponsees still have no penalties + for (PrivateKeyAccount chloeSponsee : chloeAndSponsees) + assertEquals(0, (int) new Account(repository, chloeSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that dilbert and his sponsees still have no penalties + for (PrivateKeyAccount dilbertSponsee : dilbertAndSponsees) + assertEquals(0, (int) new Account(repository, dilbertSponsee.getAddress()).getBlocksMintedPenalty()); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testMintBlockWithSignerPenalty() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + List onlineAccountsAliceSigner = new ArrayList<>(); + List onlineAccountsBobSigner = new ArrayList<>(); + + // Alice self share online, and will be used to mint (some of) the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + onlineAccountsAliceSigner.add(aliceSelfShare); + + // Bob self share online, and will be used to mint (some of) the blocks + PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share"); + onlineAccountsBobSigner.add(bobSelfShare); + + // Include Alice and Bob's online accounts in each other's arrays + onlineAccountsAliceSigner.add(bobSelfShare); + onlineAccountsBobSigner.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccountsAliceSigner.addAll(bobSponseesOnlineAccounts); + onlineAccountsBobSigner.addAll(bobSponseesOnlineAccounts); + + // Chloe sponsors 10 accounts + List chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10); + List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees); + onlineAccountsAliceSigner.addAll(chloeSponseesOnlineAccounts); + onlineAccountsBobSigner.addAll(chloeSponseesOnlineAccounts); + + // Dilbert sponsors 5 accounts + List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5); + List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees); + onlineAccountsAliceSigner.addAll(dilbertSponseesOnlineAccounts); + onlineAccountsBobSigner.addAll(dilbertSponseesOnlineAccounts); + + // Mint blocks (Bob is the signer) + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0])); + + // Get reward share transaction count + assertEquals(25, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up (Bob is the signer) + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccountsAliceSigner.addAll(bobSponseeSelfShares); + onlineAccountsBobSigner.addAll(bobSponseeSelfShares); + + // Mint blocks (Bob is the signer) + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 29 (the algo runs at block 30) (Bob is the signer) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so the algo runs (Bob is the signer) + // Block should be valid, because new account levels don't take effect until next block's validation + block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0])); + + // Ensure that bob and his sponsees now have penalties + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are now level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel()); + + // Mint a block, but Bob is now an invalid signer because he is level 0 + block = BlockMinter.mintTestingBlockUnvalidated(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0])); + // Block should be null as it's unable to be minted + assertNull(block); + + // Mint the same block with Alice as the signer, and this time it should be valid + block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0])); + // Block should NOT be null + assertNotNull(block); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testMintBlockWithFounderSignerPenalty() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + List onlineAccountsAliceSigner = new ArrayList<>(); + List onlineAccountsBobSigner = new ArrayList<>(); + + // Alice self share online, and will be used to mint (some of) the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + onlineAccountsAliceSigner.add(aliceSelfShare); + + // Bob self share online, and will be used to mint (some of) the blocks + PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share"); + onlineAccountsBobSigner.add(bobSelfShare); + + // Include Alice and Bob's online accounts in each other's arrays + onlineAccountsAliceSigner.add(bobSelfShare); + onlineAccountsBobSigner.add(aliceSelfShare); + + PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Alice sponsors 10 accounts + List aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10); + List aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees); + onlineAccountsAliceSigner.addAll(aliceSponseesOnlineAccounts); + onlineAccountsBobSigner.addAll(aliceSponseesOnlineAccounts); + + // Bob sponsors 9 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 9); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccountsAliceSigner.addAll(bobSponseesOnlineAccounts); + onlineAccountsBobSigner.addAll(bobSponseesOnlineAccounts); + + // Mint blocks (Bob is the signer) + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0])); + + // Get reward share transaction count + assertEquals(19, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + for (PrivateKeyAccount aliceSponsee : aliceSponsees) + assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up (Alice is the signer) + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount aliceSponsee : aliceSponsees) + assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees); + onlineAccountsAliceSigner.addAll(aliceSponseeSelfShares); + onlineAccountsBobSigner.addAll(aliceSponseeSelfShares); + + // Mint blocks (Bob is the signer) + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount aliceSponsee : aliceSponsees) + assertTrue(new Account(repository, aliceSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Alice then consolidates funds + consolidateFunds(repository, aliceSponsees, aliceAccount); + + // Mint until block 29 (the algo runs at block 30) (Bob is the signer) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that alice and her sponsees have no penalties + List aliceAndSponsees = new ArrayList<>(aliceSponsees); + aliceAndSponsees.add(aliceAccount); + for (PrivateKeyAccount aliceSponsee : aliceAndSponsees) + assertEquals(0, (int) new Account(repository, aliceSponsee.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so the algo runs (Alice is the signer) + // Block should be valid, because new account levels don't take effect until next block's validation + block = BlockMinter.mintTestingBlock(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0])); + + // Ensure that alice and her sponsees now have penalties + for (PrivateKeyAccount aliceSponsee : aliceAndSponsees) + assertEquals(-5000000, (int) new Account(repository, aliceSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that alice and her sponsees are now level 0 + for (PrivateKeyAccount aliceSponsee : aliceAndSponsees) + assertEquals(0, (int) new Account(repository, aliceSponsee.getAddress()).getLevel()); + + // Mint a block, but Alice is now an invalid signer because she has lost founder minting abilities + block = BlockMinter.mintTestingBlockUnvalidated(repository, onlineAccountsAliceSigner.toArray(new PrivateKeyAccount[0])); + // Block should be null as it's unable to be minted + assertNull(block); + + // Mint the same block with Bob as the signer, and this time it should be valid + block = BlockMinter.mintTestingBlock(repository, onlineAccountsBobSigner.toArray(new PrivateKeyAccount[0])); + // Block should NOT be null + assertNotNull(block); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testOnlineAccountsWithPenalties() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + // Bob self share online + PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share"); + onlineAccounts.add(bobSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Chloe sponsors 10 accounts + List chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10); + List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees); + onlineAccounts.addAll(chloeSponseesOnlineAccounts); + + // Dilbert sponsors 5 accounts + List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5); + List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees); + onlineAccounts.addAll(dilbertSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(27, block.getBlockData().getOnlineAccountsCount()); + assertEquals(25, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(bobAndSponsees, block)); + + // Ensure that chloe's sponsees are present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(chloeSponsees, block)); + + // Ensure that dilbert's sponsees are present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(dilbertSponsees, block)); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that bob and his sponsees now have penalties + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are still present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(bobAndSponsees, block)); + + // Mint another few blocks + while (block.getBlockData().getHeight() < 34) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(34, (int)block.getBlockData().getHeight()); + + // Ensure that bob and his sponsees are NOT present in block's online accounts (due to penalties) + assertFalse(areAllAccountsPresentInBlock(bobAndSponsees, block)); + + // Ensure that chloe's sponsees are still present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(chloeSponsees, block)); + + // Ensure that dilbert's sponsees are still present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(dilbertSponsees, block)); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testFounderOnlineAccountsWithPenalties() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + // Bob self share online, and will be used to mint the blocks + PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(bobSelfShare); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Alice sponsors 10 accounts + List aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10); + List aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees); + onlineAccounts.addAll(aliceSponseesOnlineAccounts); + onlineAccounts.addAll(aliceSponseesOnlineAccounts); + + // Bob sponsors 9 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 9); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Mint blocks (Bob is the signer) + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Get reward share transaction count + assertEquals(19, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + for (PrivateKeyAccount aliceSponsee : aliceSponsees) + assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up (Alice is the signer) + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount aliceSponsee : aliceSponsees) + assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees); + onlineAccounts.addAll(aliceSponseeSelfShares); + + // Mint blocks (Bob is the signer) + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount aliceSponsee : aliceSponsees) + assertTrue(new Account(repository, aliceSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Alice then consolidates funds + consolidateFunds(repository, aliceSponsees, aliceAccount); + + // Mint until block 29 (the algo runs at block 30) (Bob is the signer) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that alice and her sponsees have no penalties + List aliceAndSponsees = new ArrayList<>(aliceSponsees); + aliceAndSponsees.add(aliceAccount); + for (PrivateKeyAccount aliceSponsee : aliceAndSponsees) + assertEquals(0, (int) new Account(repository, aliceSponsee.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so the algo runs (Alice is the signer) + // Block should be valid, because new account levels don't take effect until next block's validation + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that alice and her sponsees now have penalties + for (PrivateKeyAccount aliceSponsee : aliceAndSponsees) + assertEquals(-5000000, (int) new Account(repository, aliceSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that alice and her sponsees are now level 0 + for (PrivateKeyAccount aliceSponsee : aliceAndSponsees) + assertEquals(0, (int) new Account(repository, aliceSponsee.getAddress()).getLevel()); + + // Ensure that alice and her sponsees don't have penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are still present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(bobAndSponsees, block)); + + // Ensure that alice and her sponsees are still present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(aliceAndSponsees, block)); + + // Mint another few blocks + while (block.getBlockData().getHeight() < 34) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(34, (int)block.getBlockData().getHeight()); + + // Ensure that alice and her sponsees are NOT present in block's online accounts (due to penalties) + assertFalse(areAllAccountsPresentInBlock(aliceAndSponsees, block)); + + // Ensure that bob and his sponsees are still present in block's online accounts + assertTrue(areAllAccountsPresentInBlock(bobAndSponsees, block)); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testPenaltyAccountCreateRewardShare() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Chloe sponsors 10 accounts + List chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10); + List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees); + onlineAccounts.addAll(chloeSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(21, block.getBlockData().getOnlineAccountsCount()); + assertEquals(20, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 12 (rewardshare get disabled at block 15) + while (block.getBlockData().getHeight() < 12) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(12, (int) block.getBlockData().getHeight()); + + // Bob creates a valid reward share transaction + assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount)); + + // Mint until block 30 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 30) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(30, (int) block.getBlockData().getHeight()); + + // Bob can no longer create a reward share transaction + assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, AccountUtils.createRandomRewardShare(repository, bobAccount)); + + // ... but Chloe still can + assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, chloeAccount)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Bob creates another valid reward share transaction + assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount)); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testPenaltyFounderCreateRewardShare() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + // Bob self share online, and will be used to mint the blocks + PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(bobSelfShare); + + PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Alice sponsors 10 accounts + List aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10); + List aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees); + onlineAccounts.addAll(aliceSponseesOnlineAccounts); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(21, block.getBlockData().getOnlineAccountsCount()); + assertEquals(20, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Generate self shares so the sponsees can start minting + List aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees); + onlineAccounts.addAll(aliceSponseeSelfShares); + + // Mint some blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Alice then consolidates funds + consolidateFunds(repository, aliceSponsees, aliceAccount); + + // Mint until block 12 (rewardshare get disabled at block 15) + while (block.getBlockData().getHeight() < 12) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(12, (int) block.getBlockData().getHeight()); + + // Alice creates a valid reward share transaction + assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, aliceAccount)); + + // Mint until block 30 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 30) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(30, (int) block.getBlockData().getHeight()); + + // Ensure that alice now has a penalty + assertEquals(-5000000, (int) new Account(repository, aliceAccount.getAddress()).getBlocksMintedPenalty()); + + // Ensure that alice and her sponsees are now level 0 + assertEquals(0, (int) new Account(repository, aliceAccount.getAddress()).getLevel()); + + // Alice can no longer create a reward share transaction + assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, AccountUtils.createRandomRewardShare(repository, aliceAccount)); + + // Bob can no longer create a reward share transaction (disabled at Block 15) + assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount)); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Alice creates another valid reward share transaction + assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, aliceAccount)); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + /** + * This is a test to prove that Dilbert levels up from 6 to 7 in the same block that the self + * sponsorship algo runs. It is here to give some confidence in the following testPenaltyAccountLevelUp() + * test, in which we will test what happens if a penalty is applied or removed in the same block + * that an account would otherwise have leveled up. It also gives some confidence that the algo + * doesn't affect the levels of unflagged accounts. + * + * @throws DataException + */ + @Test + public void testNonPenaltyAccountLevelUp() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Dilbert sponsors 10 accounts + List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 10); + List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees); + onlineAccounts.addAll(dilbertSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Make sure Dilbert hasn't leveled up yet + assertEquals(6, (int)dilbertAccount.getLevel()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Make sure Dilbert has leveled up + assertEquals(7, (int)dilbertAccount.getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Make sure Dilbert has returned to level 6 + assertEquals(6, (int)dilbertAccount.getLevel()); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testPenaltyAccountLevelUp() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Dilbert sponsors 10 accounts + List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 10); + List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees); + onlineAccounts.addAll(dilbertSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Generate self shares so the sponsees can start minting + List dilbertSponseeSelfShares = AccountUtils.generateSelfShares(repository, dilbertSponsees); + onlineAccounts.addAll(dilbertSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Dilbert then consolidates funds + consolidateFunds(repository, dilbertSponsees, dilbertAccount); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Make sure Dilbert hasn't leveled up yet + assertEquals(6, (int)dilbertAccount.getLevel()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Make sure Dilbert is now level 0 instead of 7 (due to penalty) + assertEquals(0, (int)dilbertAccount.getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Make sure Dilbert has returned to level 6 + assertEquals(6, (int)dilbertAccount.getLevel()); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testDuplicateSponsors() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + final int initialRewardShareCount = repository.getAccountRepository().getRewardShares().size(); + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Chloe sponsors THE SAME 10 accounts + for (PrivateKeyAccount bobSponsee : bobSponsees) { + // Create reward-share + TransactionData transactionData = AccountUtils.createRewardShare(repository, chloeAccount, bobSponsee, 0, fee); + TransactionUtils.signAndImportValid(repository, transactionData, chloeAccount); + } + List chloeSponsees = new ArrayList<>(bobSponsees); + List chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees); + onlineAccounts.addAll(chloeSponseesOnlineAccounts); + + // Dilbert sponsors 5 accounts + List dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5); + List dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees); + onlineAccounts.addAll(dilbertSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + assertEquals(26, block.getBlockData().getOnlineAccountsCount()); + assertEquals(25, repository.getAccountRepository().getRewardShares().size() - initialRewardShareCount); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + assertEquals(6, (int)dilbertAccount.getLevel()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that bob and his sponsees now have penalties + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that chloe and her sponsees also have penalties, as they relate to the same network of accounts + List chloeAndSponsees = new ArrayList<>(chloeSponsees); + chloeAndSponsees.add(chloeAccount); + for (PrivateKeyAccount chloeSponsee : chloeAndSponsees) + assertEquals(-5000000, (int) new Account(repository, chloeSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that dilbert and his sponsees have no penalties + List dilbertAndSponsees = new ArrayList<>(dilbertSponsees); + dilbertAndSponsees.add(dilbertAccount); + for (PrivateKeyAccount dilbertSponsee : dilbertAndSponsees) + assertEquals(0, (int) new Account(repository, dilbertSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are now level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure that bob and his sponsees are now greater than level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Ensure that bob and his sponsees have no penalties again + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that chloe and her sponsees still have no penalties again + for (PrivateKeyAccount chloeSponsee : chloeAndSponsees) + assertEquals(0, (int) new Account(repository, chloeSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that dilbert and his sponsees still have no penalties + for (PrivateKeyAccount dilbertSponsee : dilbertAndSponsees) + assertEquals(0, (int) new Account(repository, dilbertSponsee.getAddress()).getBlocksMintedPenalty()); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testTransferPrivsBeforeAlgoBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 28 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 28) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(28, (int) block.getBlockData().getHeight()); + + // Bob then issues a TRANSFER_PRIVS + PrivateKeyAccount recipientAccount = randomTransferPrivs(repository, bobAccount); + + // Ensure recipient has no level (actually, no account record) at this point (pre-confirmation) + assertNull(recipientAccount.getLevel()); + + // Mint another block, so that the TRANSFER_PRIVS confirms + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Now ensure that the TRANSFER_PRIVS recipient has inherited Bob's level, and Bob is at level 0 + assertTrue(recipientAccount.getLevel() > 0); + assertEquals(0, (int)bobAccount.getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob's sponsees are greater than level 0 + // Bob's account won't be, as he has transferred privs + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that bob and his sponsees now have penalties + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are now level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel()); + + // Ensure recipient account has penalty too + assertEquals(-5000000, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure that Bob's sponsees are now greater than level 0 + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Ensure that bob and his sponsees have no penalties again + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure recipient account has no penalty again and has a level greater than 0 + assertEquals(0, (int) new Account(repository, recipientAccount.getAddress()).getBlocksMintedPenalty()); + assertTrue(new Account(repository, recipientAccount.getAddress()).getLevel() > 0); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testTransferPrivsInAlgoBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Bob then issues a TRANSFER_PRIVS + PrivateKeyAccount recipientAccount = randomTransferPrivs(repository, bobAccount); + + // Ensure recipient has no level (actually, no account record) at this point (pre-confirmation) + assertNull(recipientAccount.getLevel()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Now ensure that the TRANSFER_PRIVS recipient has inherited Bob's level, and Bob is at level 0 + assertTrue(recipientAccount.getLevel() > 0); + assertEquals(0, (int)bobAccount.getLevel()); + + // Ensure that bob and his sponsees now have penalties + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are now level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure recipient has no level again + assertNull(recipientAccount.getLevel()); + + // Ensure that bob and his sponsees are now greater than level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Ensure that bob and his sponsees have no penalties again + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testTransferPrivsAfterAlgoBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 29 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 29) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(29, (int) block.getBlockData().getHeight()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Bob then issues a TRANSFER_PRIVS, which should be invalid + Transaction transferPrivsTransaction = randomTransferPrivsTransaction(repository, bobAccount); + assertEquals(ACCOUNT_NOT_TRANSFERABLE, transferPrivsTransaction.isValid()); + + // Orphan last 2 blocks + BlockUtils.orphanLastBlock(repository); + BlockUtils.orphanLastBlock(repository); + + // TRANSFER_PRIVS should now be valid + transferPrivsTransaction = randomTransferPrivsTransaction(repository, bobAccount); + assertEquals(OK, transferPrivsTransaction.isValid()); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + @Test + public void testDoubleTransferPrivs() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // Alice self share online, and will be used to mint the blocks + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + List onlineAccounts = new ArrayList<>(); + onlineAccounts.add(aliceSelfShare); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + + // Bob sponsors 10 accounts + List bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10); + List bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees); + onlineAccounts.addAll(bobSponseesOnlineAccounts); + + // Mint blocks + Block block = null; + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() == 0); + + // Mint some blocks, until accounts have leveled up + for (int i = 0; i <= 5; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Generate self shares so the sponsees can start minting + List bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees); + onlineAccounts.addAll(bobSponseeSelfShares); + + // Mint blocks + for (int i = 0; i <= 1; i++) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getConfirmedBalance(Asset.QORT) > 10); // 5 for transaction, 5 for fee + + // Bob then consolidates funds + consolidateFunds(repository, bobSponsees, bobAccount); + + // Mint until block 27 (the algo runs at block 30) + while (block.getBlockData().getHeight() < 27) + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertEquals(27, (int) block.getBlockData().getHeight()); + + // Bob then issues a TRANSFER_PRIVS + PrivateKeyAccount recipientAccount1 = randomTransferPrivs(repository, bobAccount); + + // Ensure recipient has no level (actually, no account record) at this point (pre-confirmation) + assertNull(recipientAccount1.getLevel()); + + // Bob and also sends some QORT to cover future transaction fees + // This mints another block, and the TRANSFER_PRIVS confirms + AccountUtils.pay(repository, bobAccount, recipientAccount1.getAddress(), 123456789L); + + // Now ensure that the TRANSFER_PRIVS recipient has inherited Bob's level, and Bob is at level 0 + assertTrue(recipientAccount1.getLevel() > 0); + assertEquals(0, (int)bobAccount.getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty()); + + // The recipient account then issues a TRANSFER_PRIVS of their own + PrivateKeyAccount recipientAccount2 = randomTransferPrivs(repository, recipientAccount1); + + // Ensure recipientAccount2 has no level at this point (pre-confirmation) + assertNull(recipientAccount2.getLevel()); + + // Mint another block, so that the TRANSFER_PRIVS confirms + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Now ensure that the TRANSFER_PRIVS recipient2 has inherited Bob's level, and recipient1 is at level 0 + assertTrue(recipientAccount2.getLevel() > 0); + assertEquals(0, (int)recipientAccount1.getLevel()); + assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees have no penalties + List bobAndSponsees = new ArrayList<>(bobSponsees); + bobAndSponsees.add(bobAccount); + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob's sponsees are greater than level 0 + // Bob's account won't be, as he has transferred privs + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Mint a block, so the algo runs + block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that bob and his sponsees now have penalties + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(-5000000, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure that bob and his sponsees are now level 0 + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getLevel()); + + // Ensure recipientAccount2 has penalty too + assertEquals(-5000000, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getLevel()); + + // Ensure recipientAccount1 has penalty too + assertEquals(-5000000, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getLevel()); + + // Orphan last block + BlockUtils.orphanLastBlock(repository); + + // Ensure that Bob's sponsees are now greater than level 0 + for (PrivateKeyAccount bobSponsee : bobSponsees) + assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0); + + // Ensure that bob and his sponsees have no penalties again + for (PrivateKeyAccount bobSponsee : bobAndSponsees) + assertEquals(0, (int) new Account(repository, bobSponsee.getAddress()).getBlocksMintedPenalty()); + + // Ensure recipientAccount1 has no penalty again and is level 0 + assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getBlocksMintedPenalty()); + assertEquals(0, (int) new Account(repository, recipientAccount1.getAddress()).getLevel()); + + // Ensure recipientAccount2 has no penalty again and has a level greater than 0 + assertEquals(0, (int) new Account(repository, recipientAccount2.getAddress()).getBlocksMintedPenalty()); + assertTrue(new Account(repository, recipientAccount2.getAddress()).getLevel() > 0); + + // Run orphan check - this can't be in afterTest() because some tests access the live db + Common.orphanCheck(); + } + } + + + + private static PrivateKeyAccount randomTransferPrivs(Repository repository, PrivateKeyAccount senderAccount) throws DataException { + // Generate random recipient account + byte[] randomPrivateKey = new byte[32]; + new Random().nextBytes(randomPrivateKey); + PrivateKeyAccount recipientAccount = new PrivateKeyAccount(repository, randomPrivateKey); + + BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), 0, senderAccount.getLastReference(), senderAccount.getPublicKey(), fee, null); + TransactionData transactionData = new TransferPrivsTransactionData(baseTransactionData, recipientAccount.getAddress()); + + TransactionUtils.signAndImportValid(repository, transactionData, senderAccount); + + return recipientAccount; + } + + private static TransferPrivsTransaction randomTransferPrivsTransaction(Repository repository, PrivateKeyAccount senderAccount) throws DataException { + // Generate random recipient account + byte[] randomPrivateKey = new byte[32]; + new Random().nextBytes(randomPrivateKey); + PrivateKeyAccount recipientAccount = new PrivateKeyAccount(repository, randomPrivateKey); + + BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), 0, senderAccount.getLastReference(), senderAccount.getPublicKey(), fee, null); + TransactionData transactionData = new TransferPrivsTransactionData(baseTransactionData, recipientAccount.getAddress()); + + return new TransferPrivsTransaction(repository, transactionData); + } + + private boolean areAllAccountsPresentInBlock(List accounts, Block block) throws DataException { + for (PrivateKeyAccount bobSponsee : accounts) { + boolean foundOnlineAccountInBlock = false; + for (Block.ExpandedAccount expandedAccount : block.getExpandedAccounts()) { + if (expandedAccount.getRecipientAccount().getAddress().equals(bobSponsee.getAddress())) { + foundOnlineAccountInBlock = true; + break; + } + } + if (!foundOnlineAccountInBlock) { + return false; + } + } + return true; + } + + private static void consolidateFunds(Repository repository, List sponsees, PrivateKeyAccount sponsor) throws DataException { + for (PrivateKeyAccount sponsee : sponsees) { + for (int i = 0; i < 5; i++) { + // Generate new payments from sponsee to sponsor + TransactionData paymentData = new PaymentTransactionData(TestTransaction.generateBase(sponsee), sponsor.getAddress(), 1); + TransactionUtils.signAndImportValid(repository, paymentData, sponsee); // updates paymentData's signature + } + } + } + +} \ No newline at end of file diff --git a/src/test/java/org/qortal/test/apps/CheckTranslations.java b/src/test/java/org/qortal/test/apps/CheckTranslations.java index bf239611..68322bcf 100644 --- a/src/test/java/org/qortal/test/apps/CheckTranslations.java +++ b/src/test/java/org/qortal/test/apps/CheckTranslations.java @@ -15,7 +15,7 @@ public class CheckTranslations { private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" }; private static final Set SYSTRAY_KEYS = Set.of("AUTO_UPDATE", "APPLYING_UPDATE_AND_RESTARTING", "BLOCK_HEIGHT", "BUILD_VERSION", "CHECK_TIME_ACCURACY", "CONNECTING", "CONNECTION", "CONNECTIONS", "CREATING_BACKUP_OF_DB_FILES", - "DB_BACKUP", "DB_CHECKPOINT", "EXIT", "LITE_NODE", "MINTING_DISABLED", "MINTING_ENABLED", "OPEN_UI", "PERFORMING_DB_CHECKPOINT", + "DB_BACKUP", "DB_CHECKPOINT", "EXIT", "LITE_NODE", "MINTING_DISABLED", "MINTING_ENABLED", "PERFORMING_DB_CHECKPOINT", "SYNCHRONIZE_CLOCK", "SYNCHRONIZING_BLOCKCHAIN", "SYNCHRONIZING_CLOCK"); private static String failurePrefix; diff --git a/src/test/java/org/qortal/test/at/CrowdfundTests.java b/src/test/java/org/qortal/test/at/CrowdfundTests.java new file mode 100644 index 00000000..6a60c1ab --- /dev/null +++ b/src/test/java/org/qortal/test/at/CrowdfundTests.java @@ -0,0 +1,425 @@ +package org.qortal.test.at; + +import org.ciyam.at.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.Account; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.asset.Asset; +import org.qortal.at.AT; +import org.qortal.block.Block; +import org.qortal.data.at.ATData; +import org.qortal.data.at.ATStateData; +import org.qortal.data.block.BlockData; +import org.qortal.data.transaction.*; +import org.qortal.group.Group; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.*; +import org.qortal.transaction.*; +import org.qortal.utils.*; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +public class CrowdfundTests extends Common { + + /* + "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" + "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" + "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" + "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" + */ + + private static final String aliceAddress = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v"; + private static final String bobAddress = "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK"; + private static final String chloeAddress = "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL"; + private static final String dilbertAddress = "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er"; + + // Creation bytes from: java -cp 'target/qrowdfund-1.0.0.jar:target/dependency/*' org.qortal.at.qrowdfund.Qrowdfund 50 12385 Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er + private static final String creationBytes58 = "1Pub6o13xyqfCZj8BMzmXsREVJR6h4xxpS2VPV1R2QwjP78r2ozxsNuvb28GWrT8FoTTQMGnVP7pNii6auUqYr2uunWfcxwhERbDgFdsJqtrJMpQNGB9GerAXYyiFiij35cP6eHw7BmALb3viT6VzqaXX9YB25iztekV5cTreJg7o2hRpFc9Rv8Z9dFXcD1Mm4WCaMaknUgchDi7qDnHA7JX8bn9EFD4WMG5nZHMsrmeqBHirURXr2dMxFprTBo187zztmw7izbv5KzMFP8aRP9uEqdTMhZJmvKqhapMK9UJkxMve3KnsxKn5yyaAeiZ4i9GNfrkjpz5T1VGomUaDmeatNti1bjQ2pwtcgZfFFbrnBFMU2kvcPx1UR53dArtRS7pFbNr3EFwnw2Yiu2xS3Z"; + private static final byte[] creationBytes = Base58.decode(creationBytes58); + private static final long fundingAmount = 2_00000000L; + private static final long SLEEP_PERIOD = 50L; + + private Repository repository = null; + private PrivateKeyAccount deployer; + private DeployAtTransaction deployAtTransaction; + private Account atAccount; + private String atAddress; + private byte[] rawLastTxnTimestamp = new byte[8]; + private Transaction transaction; + + @Before + public void before() throws DataException { + Common.useDefaultSettings(); + + this.repository = RepositoryManager.getRepository(); + this.deployer = Common.getTestAccount(repository, "alice"); + + this.deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + this.atAccount = deployAtTransaction.getATAccount(); + this.atAddress = deployAtTransaction.getATAccount().getAddress(); + } + + @After + public void after() throws DataException { + if (this.repository != null) + this.repository.close(); + + this.repository = null; + } + + @Test + public void testDeploy() throws DataException { + // Confirm initial value is zero + extractLastTxTimestamp(repository, atAddress, rawLastTxnTimestamp); + assertArrayEquals(new byte[8], rawLastTxnTimestamp); + } + + @Test + public void testThresholdNotMet() throws DataException { + // AT deployment in block 2 + + // Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT + BlockUtils.mintBlock(repository); // height now 3 + + // Fetch AT's balance for this height + long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT); + + // Fetch AT's initial lastTxnTimestamp + byte[] creationTimestamp = new byte[8]; + extractLastTxTimestamp(repository, atAddress, creationTimestamp); + + // Mint several blocks + int i = repository.getBlockRepository().getBlockchainHeight(); + long WAKE_HEIGHT = i + SLEEP_PERIOD; + for (; i < WAKE_HEIGHT; ++i) + BlockUtils.mintBlock(repository); + + // We should now be at WAKE_HEIGHT + long height = repository.getBlockRepository().getBlockchainHeight(); + assertEquals(WAKE_HEIGHT, height); + + // AT should have woken and run at this height so balance should have changed + + // Fetch new AT balance + long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT); + + assertNotSame(preMintBalance, postMintBalance); + + // Confirm AT has found no payments + extractLastTxTimestamp(repository, atAddress, rawLastTxnTimestamp); + assertArrayEquals(creationTimestamp, rawLastTxnTimestamp); + + // AT should have finished + ATData atData = repository.getATRepository().fromATAddress(atAddress); + assertTrue(atData.getIsFinished()); + + // AT should have sent balance back to creator + BlockData blockData = repository.getBlockRepository().getLastBlock(); + Block block = new Block(repository, blockData); + List transactions = block.getTransactions(); + + assertEquals(1, transactions.size()); + + Transaction transaction = transactions.get(0); + AtTransaction atTransaction = (AtTransaction) transaction; + assertEquals(aliceAddress, atTransaction.getRecipient().getAddress()); + } + + @Test + public void testThresholdNotMetWithOrphanage() throws DataException { + // AT deployment in block 2 + + // Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT + BlockUtils.mintBlock(repository); // height now 3 + + // Fetch AT's initial lastTxnTimestamp + byte[] creationTimestamp = new byte[8]; + extractLastTxTimestamp(repository, atAddress, creationTimestamp); + + // Mint several blocks + int i = repository.getBlockRepository().getBlockchainHeight(); + long WAKE_HEIGHT = i + SLEEP_PERIOD; + for (; i < WAKE_HEIGHT; ++i) + BlockUtils.mintBlock(repository); + + // AT should have finished + ATData atData = repository.getATRepository().fromATAddress(atAddress); + assertTrue(atData.getIsFinished()); + + // Orphan + BlockUtils.orphanBlocks(repository, 3); + + // Mint several blocks + for (i = 0; i < 3; ++i) + BlockUtils.mintBlock(repository); + + // Confirm AT has found no payments + extractLastTxTimestamp(repository, atAddress, rawLastTxnTimestamp); + assertArrayEquals(creationTimestamp, rawLastTxnTimestamp); + + // AT should have finished + atData = repository.getATRepository().fromATAddress(atAddress); + assertTrue(atData.getIsFinished()); + + // AT should have sent balance back to creator + BlockData blockData = repository.getBlockRepository().getLastBlock(); + Block block = new Block(repository, blockData); + List transactions = block.getTransactions(); + + assertEquals(1, transactions.size()); + + Transaction transaction = transactions.get(0); + AtTransaction atTransaction = (AtTransaction) transaction; + assertEquals(aliceAddress, atTransaction.getRecipient().getAddress()); + } + + @Test + public void testThresholdNotMetWithPaymentsAndRefunds() throws DataException { + // AT deployment in block 2 + + // Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT + BlockUtils.mintBlock(repository); // height now 3 + + // Fetch AT's balance for this height + long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT); + + // Fetch AT's initial lastTxnTimestamp + byte[] creationTimestamp = new byte[8]; + extractLastTxTimestamp(repository, atAddress, creationTimestamp); + + int i = repository.getBlockRepository().getBlockchainHeight(); + long WAKE_HEIGHT = i + SLEEP_PERIOD; + + // Create some test accounts, based on donations + List> donations = List.of( + new Pair<>("QRt11DVBnLaSDxr2KHvx92LdPrjhbhJtkj", 500L), + new Pair<>("QRv7tHnaEpRtfovbTJqkJFmtnoahJrbPGg", 250L), + new Pair<>("QRv7tHnaEpRtfovbTJqkJFmtnoahJrbPGg", 250L), + new Pair<>("QczG8GXU5vPQLTZsJBASQd3fAKJzKwnubv", 250L), + new Pair<>("QNuYHyW4HJn7v3dYUxoTLiyS5tpGQAguMJ", 20L), + new Pair<>("QgVqcSZZ6HRhBvdUmpTvEonaQaH2oWfe58", 500L), + new Pair<>("QfDaxmD8jKi3TovWA1NA8RL5rWYXRC12uX", 10L), + new Pair<>("QSohMWUphRwtEuwAZKqoy8UGS13tk1bBDm", 15L), + new Pair<>("QiNKXRfnX9mTodSed1yRQexhL1HA42RHHo", 420L), + new Pair<>("Qgfh143pRJyxpS92JoazjXNMH1uZueQBZ2", 100L), + new Pair<>("Qgfh143pRJyxpS92JoazjXNMH1uZueQBZ2", 100L) + ); + Map donors = donations.stream() + .map(donation -> donation.getA()) + .distinct() + .collect(Collectors.toMap(name -> name, name -> generateTestAccount(repository, name))); + + // Give donors some QORT so they can donate + donors.values() + .stream() + .forEach(donorAccount -> { + try { + AccountUtils.pay(repository, Common.getTestAccount(repository, "alice"), donorAccount.getAddress(), 2000_00000000L); + } catch (DataException e) { + fail(e.getMessage()); + } + }); + + // Record balances + Map initialDonorBalances = donors.values() + .stream() + .collect(Collectors.toMap(account -> account.getAddress(), account -> { + try { + return account.getConfirmedBalance(Asset.QORT); + } catch (DataException e) { + fail(e.getMessage()); + return null; + } + })); + + // Now make donations + donations.stream() + .forEach(donation -> { + TestAccount donorAccount = donors.get(donation.getA()); + try { + AccountUtils.pay(repository, donorAccount, atAddress, donation.getB() * 1_00000000L); + System.out.printf("AT balance at height %d is %s\n", repository.getBlockRepository().getBlockchainHeight(), Amounts.prettyAmount(atAccount.getConfirmedBalance(Asset.QORT))); + } catch (DataException e) { + fail(e.getMessage()); + } + }); + + // Mint several blocks + i = repository.getBlockRepository().getBlockchainHeight(); + for (; i < WAKE_HEIGHT; ++i) { + BlockUtils.mintBlock(repository); + System.out.printf("AT balance at height %d is %s\n", repository.getBlockRepository().getBlockchainHeight(), Amounts.prettyAmount(atAccount.getConfirmedBalance(Asset.QORT))); + } + + // We should now be at WAKE_HEIGHT + long height = repository.getBlockRepository().getBlockchainHeight(); + assertEquals(WAKE_HEIGHT, height); + + // AT should have woken and run at this height so balance should have changed + System.out.printf("AT balance at height %d is %s\n", repository.getBlockRepository().getBlockchainHeight(), Amounts.prettyAmount(atAccount.getConfirmedBalance(Asset.QORT))); + + // Fetch new AT balance + long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT); + + assertNotSame(preMintBalance, postMintBalance); + + + // Payments might happen over multiple blocks! + Map expectedBalances = new HashMap<>(initialDonorBalances); + + ATData atData; + do { + // Confirm AT has found payments + extractLastTxTimestamp(repository, atAddress, rawLastTxnTimestamp); + assertNotSame(ByteArray.wrap(creationTimestamp), ByteArray.copyOf(rawLastTxnTimestamp)); + + // AT should have sent refunds + BlockData blockData = repository.getBlockRepository().getLastBlock(); + Block block = new Block(repository, blockData); + List transactions = block.getTransactions(); + + assertNotSame(0, transactions.size()); + + // Compute expected balances + for (var transaction : transactions) { + AtTransaction atTransaction = (AtTransaction) transaction; + ATTransactionData atTransactionData = (ATTransactionData) atTransaction.getTransactionData(); + String recipient = atTransactionData.getRecipient(); + + // Skip if this is a refund to AT deployer + if (recipient.equals(aliceAddress)) + continue; + + String donorName = donors.entrySet() + .stream() + .filter(donor -> donor.getValue().getAddress().equals(recipient)) + .findFirst() + .get() + .getKey(); + System.out.printf("AT paid %s to %s\n", Amounts.prettyAmount(atTransactionData.getAmount()), donorName); + + expectedBalances.compute(atTransactionData.getRecipient(), (key, balance) -> balance - AccountUtils.fee); + } + + // AT should have finished + atData = repository.getATRepository().fromATAddress(atAddress); + + // Mint new block in case we need to loop round again + BlockUtils.mintBlock(repository); + System.out.printf("AT balance at height %d is %s\n", repository.getBlockRepository().getBlockchainHeight(), Amounts.prettyAmount(atAccount.getConfirmedBalance(Asset.QORT))); + } while (!atData.getIsFinished()); + + // Compare expected balances + donors.entrySet() + .forEach(donor -> { + String donorName = donor.getKey(); + TestAccount donorAccount = donor.getValue(); + + Long expectedBalance = expectedBalances.get(donorAccount.getAddress()); + Long actualBalance = null; + try { + actualBalance = donorAccount.getConfirmedBalance(Asset.QORT); + } catch (DataException e) { + fail(e.getMessage()); + } + + assertEquals(expectedBalance, actualBalance); + }); + } + + + private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException { + long txTimestamp = System.currentTimeMillis(); + byte[] lastReference = deployer.getLastReference(); + + if (lastReference == null) { + System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress())); + System.exit(2); + } + + Long fee = null; + String name = "Test AT"; + String description = "Test AT"; + String atType = "Test"; + String tags = "TEST"; + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null); + TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); + + DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); + + fee = deployAtTransaction.calcRecommendedFee(); + deployAtTransactionData.setFee(fee); + + TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer); + + return deployAtTransaction; + } + + private void extractLastTxTimestamp(Repository repository, String atAddress, byte[] rawLastTxnTimestamp) throws DataException { + // Check AT result + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + byte[] dataBytes = MachineState.extractDataBytes(stateData); + + System.arraycopy(dataBytes, 5 * MachineState.VALUE_SIZE, rawLastTxnTimestamp, 0, rawLastTxnTimestamp.length); + } + + private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException { + int height = transaction.getHeight(); + byte[] transactionSignature = transaction.getTransactionData().getSignature(); + + BlockData blockData = repository.getBlockRepository().fromHeight(height); + assertNotNull(blockData); + + Block block = new Block(repository, blockData); + + List blockTransactions = block.getTransactions(); + int sequence; + for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence) + if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature)) + break; + + assertNotSame(-1, sequence); + + byte[] rawLastTxTimestamp = new byte[8]; + extractLastTxTimestamp(repository, atAddress, rawLastTxTimestamp); + + Timestamp expectedTimestamp = new Timestamp(height, sequence); + Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawLastTxTimestamp, 0)); + + assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d", + height, sequence, + actualTimestamp.blockHeight, actualTimestamp.transactionSequence + ), + expectedTimestamp.longValue(), + actualTimestamp.longValue()); + + byte[] expectedPartialSignature = new byte[24]; + System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length); + + byte[] actualPartialSignature = new byte[24]; + System.arraycopy(rawLastTxTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length); + + assertArrayEquals(expectedPartialSignature, actualPartialSignature); + } + + private static TestAccount generateTestAccount(Repository repository, String accountName) { + byte[] seed = new byte[32]; + new SecureRandom().nextBytes(seed); + return new TestAccount(repository, accountName, Base58.encode(seed), false); + } + +} diff --git a/src/test/java/org/qortal/test/crosschain/BitcoinTests.java b/src/test/java/org/qortal/test/crosschain/BitcoinTests.java index e83afac2..8c41d2d2 100644 --- a/src/test/java/org/qortal/test/crosschain/BitcoinTests.java +++ b/src/test/java/org/qortal/test/crosschain/BitcoinTests.java @@ -41,24 +41,4 @@ public class BitcoinTests extends BitcoinyTests { protected String getRecipient() { return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; } - - @Test - @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") - public void testGetMedianBlockTime() {} - - @Test - @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") - public void testFindHtlcSecret() {} - - @Test - @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") - public void testBuildSpend() {} - - @Test - @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") - public void testGetWalletBalance() {} - - @Test - @Ignore("Often fails due to unreliable BTC testnet ElectrumX servers") - public void testGetUnusedReceiveAddress() {} } diff --git a/src/test/java/org/qortal/test/crosschain/BitcoinyTests.java b/src/test/java/org/qortal/test/crosschain/BitcoinyTests.java index e5486bb7..35da08d3 100644 --- a/src/test/java/org/qortal/test/crosschain/BitcoinyTests.java +++ b/src/test/java/org/qortal/test/crosschain/BitcoinyTests.java @@ -99,6 +99,14 @@ public abstract class BitcoinyTests extends Common { transaction = bitcoiny.buildSpend(xprv58, recipient, amount); assertNotNull(transaction); } + @Test + public void testRepair() throws ForeignBlockchainException { + String xprv58 = getDeterministicKey58(); + + String transaction = bitcoiny.repairOldWallet(xprv58); + + assertNotNull(transaction); + } @Test public void testGetWalletBalance() throws ForeignBlockchainException { diff --git a/src/test/java/org/qortal/test/crosschain/DigibyteTests.java b/src/test/java/org/qortal/test/crosschain/DigibyteTests.java index 9e932bbf..5387539e 100644 --- a/src/test/java/org/qortal/test/crosschain/DigibyteTests.java +++ b/src/test/java/org/qortal/test/crosschain/DigibyteTests.java @@ -29,17 +29,17 @@ public class DigibyteTests extends BitcoinyTests { @Override protected String getDeterministicKey58() { - return "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R"; + return "xprv9z8QpS7vxwMC2fCnG1oZc6c4aFRLgsqSF86yWrJBKEzMY3T3ySCo85x8Uv5FxTavAQwgEDy1g3iLRT5kdtFjoNNBKukLTMzKwCUn1Abwoxg"; } @Override protected String getDeterministicPublicKey58() { - return "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R"; + return "xpub6D7mDwepoJuVF9HFN3LZyEYo8HFq6LZHcM2aKEhnsaXLQqnCWyX3ftGcLDcjYmiPCc9GNX4VjfT32hwvYQnh9H5Z5diAvMsXRrxFmckyNoR"; } @Override protected String getRecipient() { - return "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; + return null; } @Test diff --git a/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java b/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java index c4c0acf1..7824a4a2 100644 --- a/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java +++ b/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java @@ -9,7 +9,7 @@ import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.junit.Test; import org.qortal.crosschain.Bitcoin.BitcoinNet; import org.qortal.crosschain.*; -import org.qortal.crosschain.ElectrumX.Server.ConnectionType; +import org.qortal.crosschain.ChainableServer.ConnectionType; import org.qortal.utils.BitTwiddling; import java.security.Security; diff --git a/src/test/java/org/qortal/test/crosschain/PirateChainTests.java b/src/test/java/org/qortal/test/crosschain/PirateChainTests.java index a2c2a9ec..01b9449b 100644 --- a/src/test/java/org/qortal/test/crosschain/PirateChainTests.java +++ b/src/test/java/org/qortal/test/crosschain/PirateChainTests.java @@ -259,4 +259,8 @@ public class PirateChainTests extends BitcoinyTests { @Test @Ignore(value = "Needs adapting for Pirate Chain") public void testWalletSpendingCandidateAddresses() throws ForeignBlockchainException {} + + @Test + @Ignore(value = "Needs adapting for Pirate Chain") + public void testRepair() throws ForeignBlockchainException {} } \ No newline at end of file diff --git a/src/test/java/org/qortal/test/crosschain/RavencoinTests.java b/src/test/java/org/qortal/test/crosschain/RavencoinTests.java index 3d590199..f9e6f4ae 100644 --- a/src/test/java/org/qortal/test/crosschain/RavencoinTests.java +++ b/src/test/java/org/qortal/test/crosschain/RavencoinTests.java @@ -29,12 +29,12 @@ public class RavencoinTests extends BitcoinyTests { @Override protected String getDeterministicKey58() { - return "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7"; + return "xprv9z8QpS7vxwMC2fCnG1oZc6c4aFRLgsqSF86yWrJBKEzMY3T3ySCo85x8Uv5FxTavAQwgEDy1g3iLRT5kdtFjoNNBKukLTMzKwCUn1Abwoxg"; } @Override protected String getDeterministicPublicKey58() { - return "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7"; + return "xpub6D7mDwepoJuVF9HFN3LZyEYo8HFq6LZHcM2aKEhnsaXLQqnCWyX3ftGcLDcjYmiPCc9GNX4VjfT32hwvYQnh9H5Z5diAvMsXRrxFmckyNoR"; } @Override diff --git a/src/test/java/org/qortal/test/minting/BatchRewardTests.java b/src/test/java/org/qortal/test/minting/BatchRewardTests.java index bd89384b..ad8d8d66 100644 --- a/src/test/java/org/qortal/test/minting/BatchRewardTests.java +++ b/src/test/java/org/qortal/test/minting/BatchRewardTests.java @@ -20,6 +20,7 @@ import org.qortal.settings.Settings; import org.qortal.test.common.*; import org.qortal.test.common.transaction.TestTransaction; import org.qortal.transaction.DeployAtTransaction; +import org.qortal.transaction.Transaction; import org.qortal.transform.TransformationException; import org.qortal.transform.block.BlockTransformer; import org.qortal.utils.NTP; @@ -679,4 +680,120 @@ public class BatchRewardTests extends Common { } } + @Test + public void testUnconfirmableRewardShares() throws DataException, IllegalAccessException { + // test-settings-v2-reward-scaling.json has unconfirmable reward share feature trigger enabled from block 500 + Common.useSettings("test-settings-v2-reward-scaling.json"); + + // Set reward batching to every 1000 blocks, starting at block 0, looking back the last 25 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 1000, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 25, true); + + try (final Repository repository = RepositoryManager.getRepository()) { + + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert"); + + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + PrivateKeyAccount bobSelfShare = Common.getTestAccount(repository, "bob-reward-share"); + PrivateKeyAccount chloeSelfShare = Common.getTestAccount(repository, "chloe-reward-share"); + + // Create self shares for bob, chloe and dilbert + AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert)); + + // Mint blocks 1-974 - these should have no online accounts or rewards + for (int i=1; i<974; i++) { + Block block = BlockUtils.mintBlockWithReorgs(repository, 2); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + assertFalse(block.isBatchRewardDistributionBlock()); + assertFalse(block.isOnlineAccountsBlock()); + assertEquals(0, block.getBlockData().getOnlineAccountsCount()); + } + + // Mint blocks 975-998 - these should have online accounts but no rewards + for (int i=975; i<=998; i++) { + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + assertFalse(block.isBatchRewardDistributionBlock()); + assertTrue(block.isOnlineAccountsBlock()); + assertEquals(3, block.getBlockData().getOnlineAccountsCount()); + } + + // Cancel Chloe's reward share + TransactionData transactionData = AccountUtils.createRewardShare(repository, chloe, chloe, -100, 10000000L); + TransactionUtils.signAndImportValid(repository, transactionData, chloe); + + // Mint block 999 - Chloe's account should still be included as the reward share cancellation is delayed + List onlineAccounts = Arrays.asList(aliceSelfShare, bobSelfShare, chloeSelfShare); + Block block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0])); + assertTrue(block.isBatchRewardDistributionActive()); + assertFalse(block.isRewardDistributionBlock()); + assertFalse(block.isBatchRewardDistributionBlock()); + assertTrue(block.isOnlineAccountsBlock()); + assertEquals(3, block.getBlockData().getOnlineAccountsCount()); + + // Mint block 1000 + Block block1000 = BlockUtils.mintBlockWithReorgs(repository, 12); + + // Online accounts should be included from block 999 + assertEquals(3, block1000.getBlockData().getOnlineAccountsCount()); + + assertEquals(repository.getBlockRepository().getBlockchainHeight(), 1000); + + // It's a distribution block (which is technically also an online accounts block) + assertTrue(block1000.isBatchRewardDistributionBlock()); + assertTrue(block1000.isRewardDistributionBlock()); + assertTrue(block1000.isBatchRewardDistributionActive()); + assertTrue(block1000.isOnlineAccountsBlock()); + } + } + + @Test + public void testUnconfirmableRewardShareBlocks() throws DataException, IllegalAccessException { + // test-settings-v2-reward-scaling.json has unconfirmable reward share feature trigger enabled from block 500 + Common.useSettings("test-settings-v2-reward-scaling.json"); + + // Set reward batching to every 1000 blocks, starting at block 0, looking back the last 25 blocks for online accounts + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchStartHeight", 0, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchSize", 1000, true); + FieldUtils.writeField(BlockChain.getInstance(), "blockRewardBatchAccountsBlockCount", 25, true); + + try (final Repository repository = RepositoryManager.getRepository()) { + + PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount dilbert = Common.getTestAccount(repository, "dilbert"); + + // Create self shares for bob, chloe and dilbert + AccountUtils.generateSelfShares(repository, List.of(bob, chloe, dilbert)); + + // Create transaction to cancel chloe's reward share + TransactionData rewardShareTransactionData = AccountUtils.createRewardShare(repository, chloe, chloe, -100, 10000000L); + Transaction rewardShareTransaction = Transaction.fromData(repository, rewardShareTransactionData); + + // Mint a block + BlockUtils.mintBlock(repository); + + // Check block heights up to 974 - transaction should be confirmable + for (int height=2; height<974; height++) { + assertEquals(true, rewardShareTransaction.isConfirmableAtHeight(height)); + } + + // Check block heights 975-1000 - transaction should not be confirmable + for (int height=975; height<1000; height++) { + assertEquals(false, rewardShareTransaction.isConfirmableAtHeight(height)); + } + + // Check block heights 1001-1974 - transaction should be confirmable again + for (int height=1001; height<1974; height++) { + assertEquals(true, rewardShareTransaction.isConfirmableAtHeight(height)); + } + } + } + } diff --git a/src/test/java/org/qortal/test/network/RNSNetworkTest.java b/src/test/java/org/qortal/test/network/RNSNetworkTest.java new file mode 100644 index 00000000..d81b745d --- /dev/null +++ b/src/test/java/org/qortal/test/network/RNSNetworkTest.java @@ -0,0 +1,70 @@ +package org.qortal.test.network; + +import org.apache.commons.lang3.StringUtils; +//import org.junit.Before; +//import org.junit.Ignore; +import org.junit.Test; + +//import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +//import java.util.Arrays; + +import static io.reticulum.constant.ReticulumConstant.ETC_DIR; +import static org.apache.commons.lang3.SystemUtils.USER_HOME; +//import static org.junit.Assert.assertNotNull; + +class ReticulumTest { + + //@Test + //void t() throws DecoderException { + // System.out.println(Arrays.toString(Hex.decodeHex("adf54d882c9a9b80771eb4995d702d4a3e733391b2a0f53f416d9f907e55cff8"))); + // System.out.println(2 + 1 + (128 / 8) * 2); + //} + + @Test + void path() { + System.out.println(initConfig(null)); + } + + //@Test + //void testConfigYamlParse() throws IOException { + // var config = ConfigObj.initConfig(Path.of(getSystemClassLoader().getResource("reticulum.default.yml").getPath())); + // assertNotNull(config); + //} + + //@Test + //void testHKDF() { + // var ifac_netname = "name"; + // var ifac_netkey = "password"; + // var ifacOrigin = new byte[]{}; + // ifacOrigin = ArrayUtils.addAll(ifacOrigin, getSha256Digest().digest(ifac_netname.getBytes(UTF_8))); + // ifacOrigin = ArrayUtils.addAll(ifacOrigin, getSha256Digest().digest(ifac_netkey.getBytes(UTF_8))); + // + // var ifacOriginHash = getSha256Digest().digest(ifacOrigin); + // + // var HKDF = new HKDFBytesGenerator(new SHA256Digest()); + // HKDF.init(new HKDFParameters(ifacOriginHash, IFAC_SALT, new byte[0])); + // var result = new byte[64]; + // var len = HKDF.generateBytes(result, 0, result.length); + // + // assertNotNull(Hex.encodeHexString(result)); + //} + + private String initConfig(String configDir) { + if (StringUtils.isNotBlank(configDir)) { + return configDir; + } else { + if (Files.isDirectory(Path.of(ETC_DIR)) && Files.exists(Path.of(ETC_DIR, "config"))) { + return ETC_DIR; + } else if ( + Files.isDirectory(Path.of(USER_HOME, ".config", "reticulum")) + && Files.exists(Path.of(USER_HOME, ".config", "reticulum", "config")) + ) { + return Path.of(USER_HOME, ".config", "reticulum").toString(); + } else { + return Path.of(USER_HOME, ".reticulum").toString(); + } + } + } +} diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index 2d3a6484..17fc80c4 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -18,6 +18,9 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -81,9 +84,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index 30691293..33054732 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -22,6 +22,9 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -84,9 +87,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 9b273323..577a07f1 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -85,9 +88,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index e7339947..82e4ace7 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -85,9 +88,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index fe03c37d..16032a9c 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 9999999999999, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -85,9 +88,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 9999999999999 + "arbitraryOptionalFeeTimestamp": 9999999999999, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo.json b/src/test/resources/test-chain-v2-penalty-fix.json similarity index 94% rename from src/test/resources/test-chain-v2-self-sponsorship-algo.json rename to src/test/resources/test-chain-v2-penalty-fix.json index a63a395f..e62fc9f2 100644 --- a/src/test/resources/test-chain-v2-self-sponsorship-algo.json +++ b/src/test/resources/test-chain-v2-penalty-fix.json @@ -23,6 +23,7 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -84,10 +85,15 @@ "disableReferenceTimestamp": 9999999999999, "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, - "selfSponsorshipAlgoV1Height": 20, + "selfSponsorshipAlgoV1Height": 99999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "selfSponsorshipAlgoV2Height": 9999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950, + "penaltyFixHeight": 5 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index 8acc0a35..3ec11942 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -85,9 +88,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json index 642c6415..2b8834ce 100644 --- a/src/test/resources/test-chain-v2-qora-holder-reduction.json +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -86,9 +89,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index aa1a23f3..ab96a243 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -85,9 +88,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 3073dfa9..35535c75 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -85,9 +88,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index d602de18..616d0925 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -23,6 +23,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -85,9 +88,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 500, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, @@ -107,7 +115,10 @@ { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }, { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 }, - { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 } + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 }, + { "type": "ACCOUNT_LEVEL", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 1 } ] } } diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 1261be0d..ec6ffd2e 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -22,6 +22,9 @@ "onlineAccountSignaturesMinLifetime": 3600000, "onlineAccountSignaturesMaxLifetime": 86400000, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -85,9 +88,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json new file mode 100644 index 00000000..d0d989cf --- /dev/null +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json @@ -0,0 +1,132 @@ +{ + "isTestChain": true, + "blockTimestampMargin": 500, + "transactionExpiryPeriod": 86400000, + "maxBlockSize": 2097152, + "maxBytesPerUnitFee": 0, + "unitFees": [ + { "timestamp": 0, "fee": "0.00000001" } + ], + "nameRegistrationUnitFees": [ + { "timestamp": 0, "fee": "0.00000001" }, + { "timestamp": 1645372800000, "fee": "5" } + ], + "requireGroupForApproval": false, + "minAccountLevelToRewardShare": 5, + "maxRewardSharesPerFounderMintingAccount": 20, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 20 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], + "founderEffectiveMintingLevel": 10, + "onlineAccountSignaturesMinLifetime": 3600000, + "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, + "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, + "rewardsByHeight": [ + { "height": 1, "reward": 100 }, + { "height": 11, "reward": 10 }, + { "height": 21, "reward": 1 } + ], + "sharesByLevelV1": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } + ], + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], + "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 0, + "shareBinActivationMinLevel": 7, + "blocksNeededByLevel": [ 5, 20, 30, 40, 50, 60, 18, 80, 90, 100 ], + "blockTimingsByHeight": [ + { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } + ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, + "featureTriggers": { + "messageHeight": 0, + "atHeight": 0, + "assetsTimestamp": 0, + "votingTimestamp": 0, + "arbitraryTimestamp": 0, + "powfixTimestamp": 0, + "qortalTimestamp": 0, + "newAssetPricingTimestamp": 0, + "groupApprovalTimestamp": 0, + "atFindNextTransactionFix": 0, + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, + "rewardShareLimitTimestamp": 9999999999999, + "calcChainWeightTimestamp": 0, + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0, + "disableReferenceTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "onlineAccountMinterLevelValidationHeight": 0, + "selfSponsorshipAlgoV1Height": 20, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, + "feeValidationFixTimestamp": 0, + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 + }, + "genesisInfo": { + "version": 4, + "timestamp": 0, + "transactions": [ + { "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + { "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + + { "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" }, + { "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" }, + + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + + { "type": "UPDATE_GROUP", "ownerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupId": 1, "newOwner": "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 }, + + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + + { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": "100" }, + + { "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 5 }, + { "type": "REWARD_SHARE", "minterPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "rewardSharePublicKey": "CcABzvk26TFEHG7Yok84jxyd4oBtLkx8RJdGFVz2csvp", "sharePercent": 100 }, + + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 5 }, + { "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 5 }, + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 6 } + ] + } +} diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json new file mode 100644 index 00000000..5f09cb47 --- /dev/null +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json @@ -0,0 +1,132 @@ +{ + "isTestChain": true, + "blockTimestampMargin": 500, + "transactionExpiryPeriod": 86400000, + "maxBlockSize": 2097152, + "maxBytesPerUnitFee": 0, + "unitFees": [ + { "timestamp": 0, "fee": "0.00000001" } + ], + "nameRegistrationUnitFees": [ + { "timestamp": 0, "fee": "0.00000001" }, + { "timestamp": 1645372800000, "fee": "5" } + ], + "requireGroupForApproval": false, + "minAccountLevelToRewardShare": 5, + "maxRewardSharesPerFounderMintingAccount": 20, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 20 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], + "founderEffectiveMintingLevel": 10, + "onlineAccountSignaturesMinLifetime": 3600000, + "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, + "selfSponsorshipAlgoV1SnapshotTimestamp": 0, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, + "rewardsByHeight": [ + { "height": 1, "reward": 100 }, + { "height": 11, "reward": 10 }, + { "height": 21, "reward": 1 } + ], + "sharesByLevelV1": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } + ], + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], + "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 0, + "shareBinActivationMinLevel": 7, + "blocksNeededByLevel": [ 5, 20, 30, 40, 50, 60, 18, 80, 90, 100 ], + "blockTimingsByHeight": [ + { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } + ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, + "featureTriggers": { + "messageHeight": 0, + "atHeight": 0, + "assetsTimestamp": 0, + "votingTimestamp": 0, + "arbitraryTimestamp": 0, + "powfixTimestamp": 0, + "qortalTimestamp": 0, + "newAssetPricingTimestamp": 0, + "groupApprovalTimestamp": 0, + "atFindNextTransactionFix": 0, + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, + "rewardShareLimitTimestamp": 9999999999999, + "calcChainWeightTimestamp": 0, + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0, + "disableReferenceTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "onlineAccountMinterLevelValidationHeight": 0, + "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 30, + "selfSponsorshipAlgoV3Height": 999999999, + "feeValidationFixTimestamp": 0, + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 + }, + "genesisInfo": { + "version": 4, + "timestamp": 0, + "transactions": [ + { "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + { "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + + { "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" }, + { "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" }, + + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + + { "type": "UPDATE_GROUP", "ownerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupId": 1, "newOwner": "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 }, + + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + + { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": "100" }, + + { "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 5 }, + { "type": "REWARD_SHARE", "minterPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "rewardSharePublicKey": "CcABzvk26TFEHG7Yok84jxyd4oBtLkx8RJdGFVz2csvp", "sharePercent": 100 }, + + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 5 }, + { "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 5 }, + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 6 } + ] + } +} diff --git a/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json b/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json new file mode 100644 index 00000000..f7d1faa2 --- /dev/null +++ b/src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json @@ -0,0 +1,132 @@ +{ + "isTestChain": true, + "blockTimestampMargin": 500, + "transactionExpiryPeriod": 86400000, + "maxBlockSize": 2097152, + "maxBytesPerUnitFee": 0, + "unitFees": [ + { "timestamp": 0, "fee": "0.00000001" } + ], + "nameRegistrationUnitFees": [ + { "timestamp": 0, "fee": "0.00000001" }, + { "timestamp": 1645372800000, "fee": "5" } + ], + "requireGroupForApproval": false, + "minAccountLevelToRewardShare": 5, + "maxRewardSharesPerFounderMintingAccount": 20, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 20 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], + "founderEffectiveMintingLevel": 10, + "onlineAccountSignaturesMinLifetime": 3600000, + "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, + "selfSponsorshipAlgoV1SnapshotTimestamp": 0, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, + "mempowTransactionUpdatesTimestamp": 0, + "blockRewardBatchStartHeight": 999999000, + "blockRewardBatchSize": 10, + "blockRewardBatchAccountsBlockCount": 3, + "rewardsByHeight": [ + { "height": 1, "reward": 100 }, + { "height": 11, "reward": 10 }, + { "height": 21, "reward": 1 } + ], + "sharesByLevelV1": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } + ], + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], + "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 0, + "shareBinActivationMinLevel": 7, + "blocksNeededByLevel": [ 5, 20, 30, 40, 50, 60, 28, 80, 90, 100 ], + "blockTimingsByHeight": [ + { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } + ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, + "featureTriggers": { + "messageHeight": 0, + "atHeight": 0, + "assetsTimestamp": 0, + "votingTimestamp": 0, + "arbitraryTimestamp": 0, + "powfixTimestamp": 0, + "qortalTimestamp": 0, + "newAssetPricingTimestamp": 0, + "groupApprovalTimestamp": 0, + "atFindNextTransactionFix": 0, + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, + "rewardShareLimitTimestamp": 9999999999999, + "calcChainWeightTimestamp": 0, + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0, + "disableReferenceTimestamp": 9999999999999, + "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, + "onlineAccountMinterLevelValidationHeight": 0, + "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 30, + "feeValidationFixTimestamp": 0, + "chatReferenceTimestamp": 0, + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 + }, + "genesisInfo": { + "version": 4, + "timestamp": 0, + "transactions": [ + { "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + { "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + + { "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" }, + { "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" }, + + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + + { "type": "UPDATE_GROUP", "ownerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupId": 1, "newOwner": "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 }, + + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 }, + + { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": "100" }, + + { "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 5 }, + { "type": "REWARD_SHARE", "minterPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "rewardSharePublicKey": "CcABzvk26TFEHG7Yok84jxyd4oBtLkx8RJdGFVz2csvp", "sharePercent": 100 }, + + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 5 }, + { "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 5 }, + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 6 } + ] + } +} diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 2a7aa362..542c0cf8 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -24,6 +24,9 @@ "onlineAccountSignaturesMaxLifetime": 86400000, "onlineAccountsModulusV2Timestamp": 9999999999999, "selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV2SnapshotTimestamp": 9999999999999, + "selfSponsorshipAlgoV3SnapshotTimestamp": 9999999999999, + "referenceTimestampBlock": 9999999999999, "mempowTransactionUpdatesTimestamp": 0, "blockRewardBatchStartHeight": 999999000, "blockRewardBatchSize": 10, @@ -86,9 +89,14 @@ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999, "onlineAccountMinterLevelValidationHeight": 0, "selfSponsorshipAlgoV1Height": 999999999, + "selfSponsorshipAlgoV2Height": 999999999, + "selfSponsorshipAlgoV3Height": 999999999, "feeValidationFixTimestamp": 0, "chatReferenceTimestamp": 0, - "arbitraryOptionalFeeTimestamp": 0 + "arbitraryOptionalFeeTimestamp": 0, + "unconfirmableRewardSharesHeight": 99999999, + "disableTransferPrivsTimestamp": 9999999999500, + "enableTransferPrivsTimestamp": 9999999999950 }, "genesisInfo": { "version": 4, diff --git a/src/test/resources/test-settings-v2-penalty-fix.json b/src/test/resources/test-settings-v2-penalty-fix.json new file mode 100644 index 00000000..d575e5aa --- /dev/null +++ b/src/test/resources/test-settings-v2-penalty-fix.json @@ -0,0 +1,20 @@ +{ + "repositoryPath": "testdb", + "bitcoinNet": "TEST3", + "litecoinNet": "TEST3", + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-chain-v2-penalty-fix.json", + "exportPath": "qortal-backup-test", + "bootstrap": false, + "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, + "minPeers": 0, + "pruneBlockLimit": 100, + "bootstrapFilenamePrefix": "test-", + "dataPath": "data-test", + "tempDataPath": "data-test/_temp", + "listsPath": "lists-test", + "storagePolicy": "FOLLOWED_OR_VIEWED", + "maxStorageCapacity": 104857600, + "arrrDefaultBirthday": 1900000 +} diff --git a/src/test/resources/test-settings-v2-self-sponsorship-algo.json b/src/test/resources/test-settings-v2-self-sponsorship-algo-v1.json similarity index 95% rename from src/test/resources/test-settings-v2-self-sponsorship-algo.json rename to src/test/resources/test-settings-v2-self-sponsorship-algo-v1.json index 5ea42e66..a963b125 100644 --- a/src/test/resources/test-settings-v2-self-sponsorship-algo.json +++ b/src/test/resources/test-settings-v2-self-sponsorship-algo-v1.json @@ -3,7 +3,7 @@ "bitcoinNet": "TEST3", "litecoinNet": "TEST3", "restrictedApi": false, - "blockchainConfig": "src/test/resources/test-chain-v2-self-sponsorship-algo.json", + "blockchainConfig": "src/test/resources/test-chain-v2-self-sponsorship-algo-v1.json", "exportPath": "qortal-backup-test", "bootstrap": false, "wipeUnconfirmedOnStart": false, diff --git a/src/test/resources/test-settings-v2-self-sponsorship-algo-v2.json b/src/test/resources/test-settings-v2-self-sponsorship-algo-v2.json new file mode 100644 index 00000000..430586fa --- /dev/null +++ b/src/test/resources/test-settings-v2-self-sponsorship-algo-v2.json @@ -0,0 +1,20 @@ +{ + "repositoryPath": "testdb", + "bitcoinNet": "TEST3", + "litecoinNet": "TEST3", + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-chain-v2-self-sponsorship-algo-v2.json", + "exportPath": "qortal-backup-test", + "bootstrap": false, + "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, + "minPeers": 0, + "pruneBlockLimit": 100, + "bootstrapFilenamePrefix": "test-", + "dataPath": "data-test", + "tempDataPath": "data-test/_temp", + "listsPath": "lists-test", + "storagePolicy": "FOLLOWED_OR_VIEWED", + "maxStorageCapacity": 104857600, + "arrrDefaultBirthday": 1900000 +} diff --git a/src/test/resources/test-settings-v2-self-sponsorship-algo-v3.json b/src/test/resources/test-settings-v2-self-sponsorship-algo-v3.json new file mode 100644 index 00000000..0579b1e1 --- /dev/null +++ b/src/test/resources/test-settings-v2-self-sponsorship-algo-v3.json @@ -0,0 +1,20 @@ +{ + "repositoryPath": "testdb", + "bitcoinNet": "TEST3", + "litecoinNet": "TEST3", + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-chain-v2-self-sponsorship-algo-v3.json", + "exportPath": "qortal-backup-test", + "bootstrap": false, + "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, + "minPeers": 0, + "pruneBlockLimit": 100, + "bootstrapFilenamePrefix": "test-", + "dataPath": "data-test", + "tempDataPath": "data-test/_temp", + "listsPath": "lists-test", + "storagePolicy": "FOLLOWED_OR_VIEWED", + "maxStorageCapacity": 104857600, + "arrrDefaultBirthday": 1900000 +} diff --git a/start.sh b/start.sh index a7a1419f..cc80dceb 100755 --- a/start.sh +++ b/start.sh @@ -34,7 +34,7 @@ fi # Comment out for bigger systems, e.g. non-routers # or when API documentation is enabled # Uncomment (remove '#' sign) line below if your system has less than 12GB of RAM for optimal RAM defaults -# JVM_MEMORY_ARGS="-Xss1256k -Xmx3128m" +JVM_MEMORY_ARGS="-Xss256m -XX:+UseSerialGC" # Although java.net.preferIPv4Stack is supposed to be false # by default in Java 11, on some platforms (e.g. FreeBSD 12),