mirror of
https://github.com/Qortal/qortal.git
synced 2025-11-12 19:47:04 +00:00
Compare commits
222 Commits
disable-re
...
v3.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b8fcc0a7b | ||
|
|
3d3ecbfb15 | ||
|
|
9658f0cdd4 | ||
|
|
b23500fdd0 | ||
|
|
a1365e57d8 | ||
|
|
d8ca3a455d | ||
|
|
dcc943a906 | ||
|
|
cd2010bd06 | ||
|
|
8cd16792a2 | ||
|
|
4d97586f82 | ||
|
|
3612fd8257 | ||
|
|
ff96868bd9 | ||
|
|
1694d4552e | ||
|
|
bb1593efd2 | ||
|
|
4140546afb | ||
|
|
19197812d3 | ||
|
|
168d32a474 | ||
|
|
a4fade0157 | ||
|
|
2ea6921b66 | ||
|
|
11ef31215b | ||
|
|
830a608b14 | ||
|
|
57acf7dffe | ||
|
|
9debebe03e | ||
|
|
b17e96e121 | ||
|
|
b46c3cf95f | ||
|
|
86526507a6 | ||
|
|
1b9128289f | ||
|
|
4a58f90223 | ||
|
|
e68db40d91 | ||
|
|
bd6c0c9a7d | ||
|
|
5804b9469c | ||
|
|
53b47023ac | ||
|
|
22f9f08885 | ||
|
|
f26267e572 | ||
|
|
e8c29226a1 | ||
|
|
94f48f8f54 | ||
|
|
3aac580f2c | ||
|
|
2d0b035f98 | ||
|
|
075385d3ff | ||
|
|
6ed8250301 | ||
|
|
d10ff49dcb | ||
|
|
4cf34fa932 | ||
|
|
06b5d5f1d0 | ||
|
|
d6d2641cad | ||
|
|
e71f22fd2c | ||
|
|
c996633732 | ||
|
|
55f973af3c | ||
|
|
fe9744eec6 | ||
|
|
410fa59430 | ||
|
|
522ae2bce7 | ||
|
|
a6e79947b8 | ||
|
|
fcd0d71cb6 | ||
|
|
275bee62d9 | ||
|
|
97221a4449 | ||
|
|
508a34684b | ||
|
|
3d2144f303 | ||
|
|
3c7fbed709 | ||
|
|
fb9a155e4c | ||
|
|
35f3430687 | ||
|
|
90e8cfc737 | ||
|
|
57bd3c3459 | ||
|
|
ad0d8fac91 | ||
|
|
a8b58d2007 | ||
|
|
a099ecf55b | ||
|
|
6b91b0477d | ||
|
|
d7e7c1f48c | ||
|
|
65d63487f3 | ||
|
|
7c5932a512 | ||
|
|
610a3fcf83 | ||
|
|
b329dc41bc | ||
|
|
ff78606153 | ||
|
|
ef249066cd | ||
|
|
80188629df | ||
|
|
f77093731c | ||
|
|
ca7d58c272 | ||
|
|
08f3351a7a | ||
|
|
f499ada94c | ||
|
|
f073040c06 | ||
|
|
49bfb43bd2 | ||
|
|
425c70719c | ||
|
|
1420aea600 | ||
|
|
4543062700 | ||
|
|
722468a859 | ||
|
|
492a9ed3cf | ||
|
|
420b577606 | ||
|
|
434038fd12 | ||
|
|
a9b154b783 | ||
|
|
a01652b816 | ||
|
|
4440e82bb9 | ||
|
|
a2e1efab90 | ||
|
|
7e1ce38f0a | ||
|
|
a93bae616e | ||
|
|
a2568936a0 | ||
|
|
23408827b3 | ||
|
|
ae6e2fab6f | ||
|
|
3af36644c0 | ||
|
|
db8f627f1a | ||
|
|
5db0fa080b | ||
|
|
d81071f254 | ||
|
|
ba148dfd88 | ||
|
|
dbcb457a04 | ||
|
|
b00e1c8f47 | ||
|
|
899a6eb104 | ||
|
|
6e556c82a3 | ||
|
|
35ce64cc3a | ||
|
|
09b218d16c | ||
|
|
7ea451e027 | ||
|
|
ffb27c3946 | ||
|
|
6e7d2b50a0 | ||
|
|
bd025f30ff | ||
|
|
c6cbd8e826 | ||
|
|
b85afe3ca7 | ||
|
|
5a4674c973 | ||
|
|
769418e5ae | ||
|
|
38faed5799 | ||
|
|
10a578428b | ||
|
|
96cdf4a87e | ||
|
|
c0b1580561 | ||
|
|
28f9df7178 | ||
|
|
55a0c10855 | ||
|
|
7c5165763d | ||
|
|
d2836ebcb9 | ||
|
|
fecfac5ad9 | ||
|
|
5ed1ec8809 | ||
|
|
431cbf01af | ||
|
|
af792dfc06 | ||
|
|
d3b6c5f052 | ||
|
|
f48eb27f00 | ||
|
|
b02ac2561f | ||
|
|
1b2f66b201 | ||
|
|
e992f6b683 | ||
|
|
8b3f9db497 | ||
|
|
0eebfe4a8c | ||
|
|
12b3fc257b | ||
|
|
66a3322ea6 | ||
|
|
4965cb7121 | ||
|
|
b92b1fecb0 | ||
|
|
43a75420d0 | ||
|
|
e85026f866 | ||
|
|
ba7b9f3ad8 | ||
|
|
4eb58d3591 | ||
|
|
8d8e58a905 | ||
|
|
8f58da4f52 | ||
|
|
a4e2aedde1 | ||
|
|
24d04fe928 | ||
|
|
0cf32f6c5e | ||
|
|
84d850ee0b | ||
|
|
51930d3ccf | ||
|
|
c5e5316f2e | ||
|
|
829ab1eb37 | ||
|
|
d9b330b46a | ||
|
|
c032b92d0d | ||
|
|
ae92a6eed4 | ||
|
|
712c4463f7 | ||
|
|
fbdc1e1cdb | ||
|
|
f2060fe7a1 | ||
|
|
6950c6bf69 | ||
|
|
8a76c6c0de | ||
|
|
ef51cf5702 | ||
|
|
0c3988202e | ||
|
|
987446cf7f | ||
|
|
6dd44317c4 | ||
|
|
d2fc705846 | ||
|
|
43bfd28bcd | ||
|
|
ca8f8a59f4 | ||
|
|
85a26ae052 | ||
|
|
c30b1145a1 | ||
|
|
d086ade91f | ||
|
|
64d4c458ec | ||
|
|
2478450694 | ||
|
|
5203742b05 | ||
|
|
f14b494bfc | ||
|
|
9a4ce57001 | ||
|
|
10af961fdf | ||
|
|
b17b28d9d6 | ||
|
|
e95249dc1b | ||
|
|
bb4bdfede5 | ||
|
|
e2b241d416 | ||
|
|
aeb94fb879 | ||
|
|
32213b1236 | ||
|
|
761d461bad | ||
|
|
774a3b3dcd | ||
|
|
30567d0e87 | ||
|
|
6b53eb5384 | ||
|
|
767ef62b64 | ||
|
|
f7e6d1e5c8 | ||
|
|
3f5240157e | ||
|
|
7c807f754e | ||
|
|
9e1b23caf6 | ||
|
|
c2bad62d36 | ||
|
|
4516d44cc0 | ||
|
|
9c02b01318 | ||
|
|
08fab451d2 | ||
|
|
d47570c642 | ||
|
|
4547386b1f | ||
|
|
ab01dc5e54 | ||
|
|
380c742aad | ||
|
|
368359917b | ||
|
|
1c1b570cb3 | ||
|
|
3fe43372a7 | ||
|
|
c7bc1d7dcd | ||
|
|
a7ea6ec80d | ||
|
|
9cf574b9e5 | ||
|
|
20e63a1190 | ||
|
|
f6fc5de520 | ||
|
|
0b89118cd1 | ||
|
|
e1e1a66a0b | ||
|
|
e552994f68 | ||
|
|
107a23f1ec | ||
|
|
abce068b97 | ||
|
|
28fd9241d4 | ||
|
|
3fc4746a52 | ||
|
|
598f219105 | ||
|
|
bbf7193c51 | ||
|
|
fa4679dcc4 | ||
|
|
58917eeeb4 | ||
|
|
f36e193650 | ||
|
|
999ad857ae | ||
|
|
d073b9da65 | ||
|
|
f7dabcaeb0 | ||
|
|
184984c16f | ||
|
|
2cf7a5e114 |
@@ -17,10 +17,10 @@
|
||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||
<ROW Property="NTP_GOOD" Value="false"/>
|
||||
<ROW Property="ProductCode" Value="1033:{BAC69595-0A3F-4B9F-AB21-5DAC9F288F27} 1049:{4ADB3829-2ECB-4DB0-B114-4068417B6F01} 2052:{B830ACAC-079B-49A9-8271-25D89719826C} 2057:{3F18FD2A-1F0F-4A0A-96B7-21DB829CE7EB} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{E5597539-098E-4BA6-99DF-4D22018BC0D3} 1049:{2B5E55A2-142A-4BED-B3B9-5657162282B7} 2052:{6F19171F-4743-4127-B191-AAFA3FA885D2} 2057:{A1B3108D-EC5D-47A1-AEE4-DBD956E682FB} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="3.3.1" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="3.4.3" Type="32"/>
|
||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
@@ -212,7 +212,7 @@
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{BD915701-2111-4453-8154-1E88163F5548}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{F17029E8-CCC4-456D-B4AC-1854C81C46B6}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
||||
|
||||
28
pom.xml
28
pom.xml
@@ -3,13 +3,13 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.3.2</version>
|
||||
<version>3.5.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
<altcoinj.version>6628cfd</altcoinj.version>
|
||||
<altcoinj.version>7dc8c6f</altcoinj.version>
|
||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<bouncycastle.version>1.69</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
<ciyam-at.version>1.3.8</ciyam-at.version>
|
||||
<commons-net.version>3.6</commons-net.version>
|
||||
@@ -34,6 +34,8 @@
|
||||
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
|
||||
<jsoup.version>1.13.1</jsoup.version>
|
||||
<java-diff-utils.version>4.10</java-diff-utils.version>
|
||||
<grpc.version>1.45.1</grpc.version>
|
||||
<protobuf.version>3.19.4</protobuf.version>
|
||||
</properties>
|
||||
<build>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
@@ -705,5 +707,25 @@
|
||||
<artifactId>java-diff-utils</artifactId>
|
||||
<version>${java-diff-utils.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
4499
src/main/java/cash/z/wallet/sdk/rpc/CompactFormats.java
Normal file
4499
src/main/java/cash/z/wallet/sdk/rpc/CompactFormats.java
Normal file
File diff suppressed because it is too large
Load Diff
1341
src/main/java/cash/z/wallet/sdk/rpc/CompactTxStreamerGrpc.java
Normal file
1341
src/main/java/cash/z/wallet/sdk/rpc/CompactTxStreamerGrpc.java
Normal file
File diff suppressed because it is too large
Load Diff
3854
src/main/java/cash/z/wallet/sdk/rpc/Darkside.java
Normal file
3854
src/main/java/cash/z/wallet/sdk/rpc/Darkside.java
Normal file
File diff suppressed because it is too large
Load Diff
1086
src/main/java/cash/z/wallet/sdk/rpc/DarksideStreamerGrpc.java
Normal file
1086
src/main/java/cash/z/wallet/sdk/rpc/DarksideStreamerGrpc.java
Normal file
File diff suppressed because it is too large
Load Diff
15106
src/main/java/cash/z/wallet/sdk/rpc/Service.java
Normal file
15106
src/main/java/cash/z/wallet/sdk/rpc/Service.java
Normal file
File diff suppressed because it is too large
Load Diff
100
src/main/java/com/rust/litewalletjni/LiteWalletJni.java
Normal file
100
src/main/java/com/rust/litewalletjni/LiteWalletJni.java
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* LiteWalletJni code based on https://github.com/PirateNetwork/cordova-plugin-litewallet
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 Zero Currency Coin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.rust.litewalletjni;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.PirateChainWalletController;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class LiteWalletJni {
|
||||
|
||||
protected static final Logger LOGGER = LogManager.getLogger(LiteWalletJni.class);
|
||||
|
||||
public static native String initlogging();
|
||||
public static native String initnew(final String serveruri, final String params, final String saplingOutputb64, final String saplingSpendb64);
|
||||
public static native String initfromseed(final String serveruri, final String params, final String seed, final String birthday, final String saplingOutputb64, final String saplingSpendb64);
|
||||
public static native String initfromb64(final String serveruri, final String params, final String datab64, final String saplingOutputb64, final String saplingSpendb64);
|
||||
public static native String save();
|
||||
|
||||
public static native String execute(final String cmd, final String args);
|
||||
public static native String getseedphrase();
|
||||
public static native String getseedphrasefromentropyb64(final String entropy64);
|
||||
public static native String checkseedphrase(final String input);
|
||||
|
||||
|
||||
private static boolean loaded = false;
|
||||
|
||||
public static void loadLibrary() {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArchitecture = System.getProperty("os.arch");
|
||||
|
||||
LOGGER.info("OS Name: {}", osName);
|
||||
LOGGER.info("OS Architecture: {}", osArchitecture);
|
||||
|
||||
try {
|
||||
String libFileName = PirateChainWalletController.getRustLibFilename();
|
||||
if (libFileName == null) {
|
||||
LOGGER.info("Library not found for OS: {}, arch: {}", osName, osArchitecture);
|
||||
return;
|
||||
}
|
||||
|
||||
Path libPath = Paths.get(PirateChainWalletController.getRustLibOuterDirectory().toString(), libFileName);
|
||||
System.load(libPath.toAbsolutePath().toString());
|
||||
loaded = true;
|
||||
}
|
||||
catch (UnsatisfiedLinkError e) {
|
||||
LOGGER.info("Unable to load library");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.Security;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -18,6 +19,8 @@ import org.qortal.api.ApiRequest;
|
||||
import org.qortal.controller.AutoUpdate;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import static org.qortal.controller.AutoUpdate.AGENTLIB_JVM_HOLDER_ARG;
|
||||
|
||||
public class ApplyUpdate {
|
||||
|
||||
static {
|
||||
@@ -197,6 +200,11 @@ public class ApplyUpdate {
|
||||
// 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));
|
||||
|
||||
@@ -205,7 +213,7 @@ public class ApplyUpdate {
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info(() -> String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||
|
||||
@@ -214,8 +222,15 @@ public class ApplyUpdate {
|
||||
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
||||
}
|
||||
|
||||
processBuilder.start();
|
||||
} catch (IOException e) {
|
||||
// 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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ public class PrivateKeyAccount extends PublicKeyAccount {
|
||||
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
|
||||
|
||||
/**
|
||||
* Create PrivateKeyAccount using byte[32] seed.
|
||||
* Create PrivateKeyAccount using byte[32] private key.
|
||||
*
|
||||
* @param seed
|
||||
* @param privateKey
|
||||
* byte[32] used to create private/public key pair
|
||||
* @throws IllegalArgumentException
|
||||
* if passed invalid seed
|
||||
* if passed invalid privateKey
|
||||
*/
|
||||
public PrivateKeyAccount(Repository repository, byte[] seed) {
|
||||
this(repository, new Ed25519PrivateKeyParameters(seed, 0));
|
||||
public PrivateKeyAccount(Repository repository, byte[] privateKey) {
|
||||
this(repository, new Ed25519PrivateKeyParameters(privateKey, 0));
|
||||
}
|
||||
|
||||
private PrivateKeyAccount(Repository repository, Ed25519PrivateKeyParameters edPrivateKeyParams) {
|
||||
@@ -37,10 +37,6 @@ public class PrivateKeyAccount extends PublicKeyAccount {
|
||||
return this.privateKey;
|
||||
}
|
||||
|
||||
public static byte[] toPublicKey(byte[] seed) {
|
||||
return new Ed25519PrivateKeyParameters(seed, 0).generatePublicKey().getEncoded();
|
||||
}
|
||||
|
||||
public byte[] sign(byte[] message) {
|
||||
return Crypto.sign(this.edPrivateKeyParams, message);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.network.Network;
|
||||
|
||||
@@ -21,7 +22,7 @@ public class NodeStatus {
|
||||
public final int height;
|
||||
|
||||
public NodeStatus() {
|
||||
this.isMintingPossible = Controller.getInstance().isMintingPossible();
|
||||
this.isMintingPossible = OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures();
|
||||
|
||||
this.syncPercent = Synchronizer.getInstance().getSyncPercent();
|
||||
this.isSynchronizing = Synchronizer.getInstance().isSynchronizing();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class PirateChainSendRequest {
|
||||
|
||||
@Schema(description = "32 bytes of entropy, Base58 encoded", example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV")
|
||||
public String entropy58;
|
||||
|
||||
@Schema(description = "Recipient's Pirate Chain address", example = "zc...")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of ARRR to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long arrrAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 ARRR (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
@Schema(description = "Optional memo to include information for the recipient", example = "zc...")
|
||||
public String memo;
|
||||
|
||||
public PirateChainSendRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -125,12 +125,12 @@ public class AdminResource {
|
||||
}
|
||||
|
||||
private String getNodeType() {
|
||||
if (Settings.getInstance().isTopOnly()) {
|
||||
return "topOnly";
|
||||
}
|
||||
else if (Settings.getInstance().isLite()) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
return "lite";
|
||||
}
|
||||
else if (Settings.getInstance().isTopOnly()) {
|
||||
return "topOnly";
|
||||
}
|
||||
else {
|
||||
return "full";
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
@@ -56,7 +55,9 @@ import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.ZipUtils;
|
||||
|
||||
@Path("/arbitrary")
|
||||
@@ -253,7 +254,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
|
||||
return this.getStatus(service, name, null, build);
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, null, build);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -275,7 +276,7 @@ public class ArbitraryResource {
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
|
||||
return this.getStatus(service, name, identifier, build);
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, identifier, build);
|
||||
}
|
||||
|
||||
|
||||
@@ -1099,7 +1100,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, error);
|
||||
}
|
||||
|
||||
if (!Controller.getInstance().isUpToDate()) {
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
|
||||
}
|
||||
|
||||
@@ -1245,24 +1247,6 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
|
||||
private ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
|
||||
|
||||
// If "build=true" has been specified in the query string, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
try {
|
||||
if (!reader.isBuilding()) {
|
||||
reader.loadSynchronously(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// No need to handle exception, as it will be reflected in the status
|
||||
}
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
|
||||
@@ -114,7 +114,7 @@ public class BlocksResource {
|
||||
@Path("/signature/{signature}/data")
|
||||
@Operation(
|
||||
summary = "Fetch serialized, base58 encoded block data using base58 signature",
|
||||
description = "Returns serialized data for the block that matches the given signature",
|
||||
description = "Returns serialized data for the block that matches the given signature, and an optional block serialization version",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "the block data",
|
||||
@@ -125,7 +125,7 @@ public class BlocksResource {
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public String getSerializedBlockData(@PathParam("signature") String signature58) {
|
||||
public String getSerializedBlockData(@PathParam("signature") String signature58, @QueryParam("version") Integer version) {
|
||||
// Decode signature
|
||||
byte[] signature;
|
||||
try {
|
||||
@@ -136,20 +136,41 @@ public class BlocksResource {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Default to version 1
|
||||
if (version == null) {
|
||||
version = 1;
|
||||
}
|
||||
|
||||
// Check the database first
|
||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
Block block = new Block(repository, blockData);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
|
||||
switch (version) {
|
||||
case 1:
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
bytes.write(BlockTransformer.toBytesV2(block));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
|
||||
return Base58.encode(bytes.toByteArray());
|
||||
}
|
||||
|
||||
// Not found, so try the block archive
|
||||
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
|
||||
if (bytes != null) {
|
||||
return Base58.encode(bytes);
|
||||
if (version != 1) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Archived blocks require version 1");
|
||||
}
|
||||
return Base58.encode(bytes);
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.PirateChainSendRequest;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.PirateChain;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/crosschain/arrr")
|
||||
@Tag(name = "Cross-Chain (Pirate Chain)")
|
||||
public class CrossChainPirateChainResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@POST
|
||||
@Path("/walletbalance")
|
||||
@Operation(
|
||||
summary = "Returns ARRR balance",
|
||||
description = "Supply 32 bytes of entropy, Base58 encoded",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "32 bytes of entropy, Base58 encoded",
|
||||
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String getPirateChainWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
try {
|
||||
Long balance = pirateChain.getWalletBalance(entropy58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
return balance.toString();
|
||||
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/wallettransactions")
|
||||
@Operation(
|
||||
summary = "Returns transactions",
|
||||
description = "Supply 32 bytes of entropy, Base58 encoded",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "32 bytes of entropy, Base58 encoded",
|
||||
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||
)
|
||||
)
|
||||
),
|
||||
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 List<SimpleTransaction> getPirateChainWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
try {
|
||||
return pirateChain.getWalletTransactions(entropy58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/send")
|
||||
@Operation(
|
||||
summary = "Sends ARRR from wallet",
|
||||
description = "Currently supports 'legacy' P2PKH PirateChain addresses and Native SegWit (P2WPKH) addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "32 bytes of entropy, Base58 encoded",
|
||||
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String sendBitcoin(@HeaderParam(Security.API_KEY_HEADER) String apiKey, PirateChainSendRequest pirateChainSendRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (pirateChainSendRequest.arrrAmount <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
if (pirateChainSendRequest.feePerByte != null && pirateChainSendRequest.feePerByte <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
try {
|
||||
return pirateChain.sendCoins(pirateChainSendRequest);
|
||||
|
||||
} catch (ForeignBlockchainException e) {
|
||||
// TODO
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Path("/walletaddress")
|
||||
@Operation(
|
||||
summary = "Returns main wallet address",
|
||||
description = "Supply 32 bytes of entropy, Base58 encoded",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "32 bytes of entropy, Base58 encoded",
|
||||
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||
)
|
||||
)
|
||||
),
|
||||
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 getPirateChainWalletAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
try {
|
||||
return pirateChain.getWalletAddress(entropy58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Path("/syncstatus")
|
||||
@Operation(
|
||||
summary = "Returns synchronization status",
|
||||
description = "Supply 32 bytes of entropy, Base58 encoded",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "32 bytes of entropy, Base58 encoded",
|
||||
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||
)
|
||||
)
|
||||
),
|
||||
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 getPirateChainSyncStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
try {
|
||||
return pirateChain.getSyncStatus(entropy58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@Path("/crosschain/tradebot")
|
||||
@Tag(name = "Cross-Chain (Trade-Bot)")
|
||||
@@ -137,7 +138,8 @@ public class CrossChainTradeBotResource {
|
||||
if (tradeBotCreateRequest.qortAmount <= 0 || tradeBotCreateRequest.fundingQortAmount <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_SIZE_TOO_SMALL);
|
||||
|
||||
if (!Controller.getInstance().isUpToDate())
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -153,7 +155,7 @@ public class CrossChainTradeBotResource {
|
||||
|
||||
return Base58.encode(unsignedBytes);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +200,8 @@ public class CrossChainTradeBotResource {
|
||||
if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (!Controller.getInstance().isUpToDate())
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
@@ -237,7 +240,7 @@ public class CrossChainTradeBotResource {
|
||||
return "false";
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -723,9 +723,9 @@ public class TransactionsResource {
|
||||
ApiError.BLOCKCHAIN_NEEDS_SYNC, ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public String processTransaction(String rawBytes58) {
|
||||
// Only allow a transaction to be processed if our latest block is less than 30 minutes old
|
||||
// Only allow a transaction to be processed if our latest block is less than 60 minutes old
|
||||
// If older than this, we should first wait until the blockchain is synced
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
|
||||
|
||||
@@ -748,7 +748,7 @@ public class TransactionsResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
|
||||
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS))
|
||||
if (!blockchainLock.tryLock(60, TimeUnit.SECONDS))
|
||||
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
|
||||
|
||||
try {
|
||||
|
||||
@@ -170,6 +170,7 @@ public class ArbitraryDataReader {
|
||||
this.validate();
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("DataException when trying to load QDN resource", e);
|
||||
this.deleteWorkingDirectory();
|
||||
throw new DataException(e.getMessage());
|
||||
|
||||
@@ -208,8 +209,13 @@ public class ArbitraryDataReader {
|
||||
* serve a cached version of the resource for subsequent requests.
|
||||
* @throws IOException
|
||||
*/
|
||||
private void deleteWorkingDirectory() throws IOException {
|
||||
FilesystemUtils.safeDeleteDirectory(this.workingPath, true);
|
||||
private void deleteWorkingDirectory() {
|
||||
try {
|
||||
FilesystemUtils.safeDeleteDirectory(this.workingPath, true);
|
||||
} catch (IOException e) {
|
||||
// Ignore failures as this isn't an essential step
|
||||
LOGGER.info("Unable to delete working path {}: {}", this.workingPath, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void createUncompressedDirectory() throws DataException {
|
||||
@@ -408,6 +414,7 @@ public class ArbitraryDataReader {
|
||||
this.decryptUsingAlgo("AES/CBC/PKCS5Padding");
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to decrypt using specific parameters: {}", e.getMessage());
|
||||
// Something went wrong, so fall back to default AES params (necessary for legacy resource support)
|
||||
this.decryptUsingAlgo("AES");
|
||||
|
||||
@@ -420,8 +427,9 @@ public class ArbitraryDataReader {
|
||||
byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null;
|
||||
if (secret != null && secret.length == Transformer.AES256_LENGTH) {
|
||||
try {
|
||||
LOGGER.info("Decrypting using algorithm {}...", algorithm);
|
||||
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
|
||||
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, algorithm);
|
||||
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES");
|
||||
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
|
||||
|
||||
// Replace filePath pointer with the encrypted file path
|
||||
@@ -430,7 +438,8 @@ public class ArbitraryDataReader {
|
||||
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
|
||||
| BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) {
|
||||
throw new DataException(String.format("Unable to decrypt file at path %s: %s", this.filePath, e.getMessage()));
|
||||
LOGGER.info(String.format("Exception when decrypting using algorithm %s", algorithm), e);
|
||||
throw new DataException(String.format("Unable to decrypt file at path %s using algorithm %s: %s", this.filePath, algorithm, e.getMessage()));
|
||||
}
|
||||
} else {
|
||||
// Assume it is unencrypted. This will be the case when we have built a custom path by combining
|
||||
@@ -477,7 +486,12 @@ public class ArbitraryDataReader {
|
||||
// Delete original compressed file
|
||||
if (FilesystemUtils.pathInsideDataOrTempPath(this.filePath)) {
|
||||
if (Files.exists(this.filePath)) {
|
||||
Files.delete(this.filePath);
|
||||
try {
|
||||
Files.delete(this.filePath);
|
||||
} catch (IOException e) {
|
||||
// Ignore failures as this isn't an essential step
|
||||
LOGGER.info("Unable to delete file at path {}", this.filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,14 @@ package org.qortal.block;
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -24,6 +28,7 @@ import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||
import org.qortal.block.BlockChain.AccountLevelShareBin;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.Qortal25519Extras;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.EligibleQoraHolderData;
|
||||
@@ -118,6 +123,8 @@ public class Block {
|
||||
|
||||
/** Remote/imported/loaded AT states */
|
||||
protected List<ATStateData> atStates;
|
||||
/** Remote hash of AT states - in lieu of full AT state data in {@code atStates} */
|
||||
protected byte[] atStatesHash;
|
||||
/** Locally-generated AT states */
|
||||
protected List<ATStateData> ourAtStates;
|
||||
/** Locally-generated AT fees */
|
||||
@@ -192,6 +199,11 @@ public class Block {
|
||||
|
||||
}
|
||||
|
||||
public boolean hasShareBin(AccountLevelShareBin shareBin, int blockHeight) {
|
||||
AccountLevelShareBin ourShareBin = this.getShareBin(blockHeight);
|
||||
return ourShareBin != null && shareBin.id == ourShareBin.id;
|
||||
}
|
||||
|
||||
public long distribute(long accountAmount, Map<String, Long> balanceChanges) {
|
||||
if (this.isRecipientAlsoMinter) {
|
||||
// minter & recipient the same - simpler case
|
||||
@@ -216,11 +228,10 @@ public class Block {
|
||||
return accountAmount;
|
||||
}
|
||||
}
|
||||
|
||||
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
|
||||
private List<ExpandedAccount> cachedExpandedAccounts = null;
|
||||
|
||||
/** Opportunistic cache of this block's valid online accounts. Only created by call to isValid(). */
|
||||
private List<OnlineAccountData> cachedValidOnlineAccounts = null;
|
||||
/** Opportunistic cache of this block's valid online reward-shares. Only created by call to isValid(). */
|
||||
private List<RewardShareData> cachedOnlineRewardShares = null;
|
||||
|
||||
@@ -255,7 +266,7 @@ public class Block {
|
||||
* Constructs new Block using passed transaction and AT states.
|
||||
* <p>
|
||||
* This constructor typically used when receiving a serialized block over the network.
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
* @param blockData
|
||||
* @param transactions
|
||||
@@ -281,6 +292,35 @@ public class Block {
|
||||
this.blockData.setTotalFees(totalFees);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs new Block using passed transaction and minimal AT state info.
|
||||
* <p>
|
||||
* This constructor typically used when receiving a serialized block over the network.
|
||||
*
|
||||
* @param repository
|
||||
* @param blockData
|
||||
* @param transactions
|
||||
* @param atStatesHash
|
||||
*/
|
||||
public Block(Repository repository, BlockData blockData, List<TransactionData> transactions, byte[] atStatesHash) {
|
||||
this(repository, blockData);
|
||||
|
||||
this.transactions = new ArrayList<>();
|
||||
|
||||
long totalFees = 0;
|
||||
|
||||
// We have to sum fees too
|
||||
for (TransactionData transactionData : transactions) {
|
||||
this.transactions.add(Transaction.fromData(repository, transactionData));
|
||||
totalFees += transactionData.getFee();
|
||||
}
|
||||
|
||||
this.atStatesHash = atStatesHash;
|
||||
totalFees += this.blockData.getATFees();
|
||||
|
||||
this.blockData.setTotalFees(totalFees);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs new Block with empty transaction list, using passed minter account.
|
||||
*
|
||||
@@ -313,18 +353,21 @@ public class Block {
|
||||
int version = parentBlock.getNextBlockVersion();
|
||||
byte[] reference = parentBlockData.getSignature();
|
||||
|
||||
// Fetch our list of online accounts
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
LOGGER.error("No online accounts - not even our own?");
|
||||
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
|
||||
if (minterLevel == 0) {
|
||||
LOGGER.error("Minter effective level returned zero?");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find newest online accounts timestamp
|
||||
long onlineAccountsTimestamp = 0;
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
if (onlineAccountData.getTimestamp() > onlineAccountsTimestamp)
|
||||
onlineAccountsTimestamp = onlineAccountData.getTimestamp();
|
||||
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
|
||||
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
|
||||
|
||||
// Fetch our list of online accounts
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
LOGGER.error("No online accounts - not even our own?");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
|
||||
@@ -335,10 +378,6 @@ public class Block {
|
||||
// Map using index into sorted list of reward-shares as key
|
||||
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
// Disregard online accounts with different timestamps
|
||||
if (onlineAccountData.getTimestamp() != onlineAccountsTimestamp)
|
||||
continue;
|
||||
|
||||
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
|
||||
if (accountIndex == null)
|
||||
// Online account (reward-share) with current timestamp but reward-share cancelled
|
||||
@@ -355,26 +394,29 @@ public class Block {
|
||||
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
|
||||
int onlineAccountsCount = onlineAccountsSet.size();
|
||||
|
||||
// Concatenate online account timestamp signatures (in correct order)
|
||||
byte[] onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH];
|
||||
for (int i = 0; i < onlineAccountsCount; ++i) {
|
||||
Integer accountIndex = accountIndexes.get(i);
|
||||
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
|
||||
System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
|
||||
byte[] onlineAccountsSignatures;
|
||||
if (timestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
|
||||
// Collate all signatures
|
||||
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
|
||||
.stream()
|
||||
.map(OnlineAccountData::getSignature)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Aggregated, single signature
|
||||
onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
|
||||
} else {
|
||||
// Concatenate online account timestamp signatures (in correct order)
|
||||
onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH];
|
||||
for (int i = 0; i < onlineAccountsCount; ++i) {
|
||||
Integer accountIndex = accountIndexes.get(i);
|
||||
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
|
||||
System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
|
||||
minter.getPublicKey(), encodedOnlineAccounts));
|
||||
|
||||
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
|
||||
if (minterLevel == 0) {
|
||||
LOGGER.error("Minter effective level returned zero?");
|
||||
return null;
|
||||
}
|
||||
|
||||
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
|
||||
|
||||
int transactionCount = 0;
|
||||
byte[] transactionsSignature = null;
|
||||
int height = parentBlockData.getHeight() + 1;
|
||||
@@ -979,49 +1021,59 @@ public class Block {
|
||||
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
|
||||
|
||||
if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
||||
if (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
|
||||
// We expect just the one, aggregated signature
|
||||
if (this.blockData.getOnlineAccountsSignatures().length != Transformer.SIGNATURE_LENGTH)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
||||
} else {
|
||||
if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
||||
}
|
||||
|
||||
// Check signatures
|
||||
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
|
||||
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);
|
||||
|
||||
// If this block is much older than current online timestamp, then there's no point checking current online accounts
|
||||
List<OnlineAccountData> currentOnlineAccounts = onlineTimestamp < NTP.getTime() - OnlineAccountsManager.ONLINE_TIMESTAMP_MODULUS
|
||||
? null
|
||||
: OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> latestBlocksOnlineAccounts = OnlineAccountsManager.getInstance().getLatestBlocksOnlineAccounts();
|
||||
|
||||
// Extract online accounts' timestamp signatures from block data
|
||||
// Extract online accounts' timestamp signatures from block data. Only one signature if aggregated.
|
||||
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
|
||||
|
||||
// We'll build up a list of online accounts to hand over to Controller if block is added to chain
|
||||
// and this will become latestBlocksOnlineAccounts (above) to reduce CPU load when we process next block...
|
||||
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
|
||||
if (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
|
||||
// Aggregate all public keys
|
||||
Collection<byte[]> publicKeys = onlineRewardShares.stream()
|
||||
.map(RewardShareData::getRewardSharePublicKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
|
||||
byte[] signature = onlineAccountsSignatures.get(i);
|
||||
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
|
||||
byte[] aggregatePublicKey = Qortal25519Extras.aggregatePublicKeys(publicKeys);
|
||||
|
||||
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
|
||||
ourOnlineAccounts.add(onlineAccountData);
|
||||
byte[] aggregateSignature = onlineAccountsSignatures.get(0);
|
||||
|
||||
// If signature is still current then no need to perform Ed25519 verify
|
||||
if (currentOnlineAccounts != null && currentOnlineAccounts.remove(onlineAccountData))
|
||||
// remove() returned true, so online account still current
|
||||
// and one less entry in currentOnlineAccounts to check next time
|
||||
continue;
|
||||
|
||||
// If signature was okay in latest block then no need to perform Ed25519 verify
|
||||
if (latestBlocksOnlineAccounts != null && latestBlocksOnlineAccounts.contains(onlineAccountData))
|
||||
continue;
|
||||
|
||||
if (!Crypto.verify(publicKey, signature, onlineTimestampBytes))
|
||||
// One-step verification of aggregate signature using aggregate public key
|
||||
if (!Qortal25519Extras.verifyAggregated(aggregatePublicKey, aggregateSignature, onlineTimestampBytes))
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
|
||||
} else {
|
||||
// Build block's view of online accounts
|
||||
Set<OnlineAccountData> onlineAccounts = new HashSet<>();
|
||||
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
|
||||
byte[] signature = onlineAccountsSignatures.get(i);
|
||||
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
|
||||
|
||||
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
|
||||
onlineAccounts.add(onlineAccountData);
|
||||
}
|
||||
|
||||
// Remove those already validated & cached by online accounts manager - no need to re-validate them
|
||||
OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
|
||||
|
||||
// Validate the rest
|
||||
for (OnlineAccountData onlineAccount : onlineAccounts)
|
||||
if (!Crypto.verify(onlineAccount.getPublicKey(), onlineAccount.getSignature(), onlineTimestampBytes))
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
|
||||
|
||||
// We've validated these, so allow online accounts manager to cache
|
||||
OnlineAccountsManager.getInstance().addBlocksOnlineAccounts(onlineAccounts, onlineTimestamp);
|
||||
}
|
||||
|
||||
// All online accounts valid, so save our list of online accounts for potential later use
|
||||
this.cachedValidOnlineAccounts = ourOnlineAccounts;
|
||||
this.cachedOnlineRewardShares = onlineRewardShares;
|
||||
|
||||
return ValidationResult.OK;
|
||||
@@ -1168,6 +1220,7 @@ public class Block {
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("DataException during transaction validation", e);
|
||||
return ValidationResult.TRANSACTION_INVALID;
|
||||
} finally {
|
||||
// Rollback repository changes made by test-processing transactions above
|
||||
@@ -1194,7 +1247,7 @@ public class Block {
|
||||
*/
|
||||
private ValidationResult areAtsValid() throws DataException {
|
||||
// Locally generated AT states should be valid so no need to re-execute them
|
||||
if (this.ourAtStates == this.getATStates()) // Note object reference compare
|
||||
if (this.ourAtStates != null && this.ourAtStates == this.atStates) // Note object reference compare
|
||||
return ValidationResult.OK;
|
||||
|
||||
// Generate local AT states for comparison
|
||||
@@ -1208,8 +1261,33 @@ public class Block {
|
||||
if (this.ourAtFees != this.blockData.getATFees())
|
||||
return ValidationResult.AT_STATES_MISMATCH;
|
||||
|
||||
// Note: this.atStates fully loaded thanks to this.getATStates() call above
|
||||
for (int s = 0; s < this.atStates.size(); ++s) {
|
||||
// If we have a single AT states hash then compare that in preference
|
||||
if (this.atStatesHash != null) {
|
||||
int atBytesLength = blockData.getATCount() * BlockTransformer.AT_ENTRY_LENGTH;
|
||||
ByteArrayOutputStream atHashBytes = new ByteArrayOutputStream(atBytesLength);
|
||||
|
||||
try {
|
||||
for (ATStateData atStateData : this.ourAtStates) {
|
||||
atHashBytes.write(atStateData.getATAddress().getBytes(StandardCharsets.UTF_8));
|
||||
atHashBytes.write(atStateData.getStateHash());
|
||||
atHashBytes.write(Longs.toByteArray(atStateData.getFees()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DataException("Couldn't validate AT states hash due to serialization issue?", e);
|
||||
}
|
||||
|
||||
byte[] ourAtStatesHash = Crypto.digest(atHashBytes.toByteArray());
|
||||
if (!Arrays.equals(ourAtStatesHash, this.atStatesHash))
|
||||
return ValidationResult.AT_STATES_MISMATCH;
|
||||
|
||||
// Use our AT state data from now on
|
||||
this.atStates = this.ourAtStates;
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
// Note: this.atStates fully loaded thanks to this.getATStates() call:
|
||||
this.getATStates();
|
||||
for (int s = 0; s < this.ourAtStates.size(); ++s) {
|
||||
ATStateData ourAtState = this.ourAtStates.get(s);
|
||||
ATStateData theirAtState = this.atStates.get(s);
|
||||
|
||||
@@ -1367,9 +1445,6 @@ public class Block {
|
||||
|
||||
postBlockTidy();
|
||||
|
||||
// Give Controller our cached, valid online accounts data (if any) to help reduce CPU load for next block
|
||||
OnlineAccountsManager.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
|
||||
|
||||
// Log some debugging info relating to the block weight calculation
|
||||
this.logDebugInfo();
|
||||
}
|
||||
@@ -1585,9 +1660,6 @@ public class Block {
|
||||
this.blockData.setHeight(null);
|
||||
|
||||
postBlockTidy();
|
||||
|
||||
// Remove any cached, valid online accounts data from Controller
|
||||
OnlineAccountsManager.getInstance().popLatestBlocksOnlineAccounts();
|
||||
}
|
||||
|
||||
protected void orphanTransactionsFromBlock() throws DataException {
|
||||
@@ -1825,12 +1897,67 @@ public class Block {
|
||||
final boolean haveFounders = !onlineFounderAccounts.isEmpty();
|
||||
|
||||
// Determine reward candidates based on account level
|
||||
List<AccountLevelShareBin> accountLevelShareBins = BlockChain.getInstance().getAccountLevelShareBins();
|
||||
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
|
||||
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
|
||||
// This needs a deep copy, so the shares can be modified when tiers aren't activated yet
|
||||
List<AccountLevelShareBin> accountLevelShareBins = new ArrayList<>();
|
||||
for (AccountLevelShareBin accountLevelShareBin : BlockChain.getInstance().getAccountLevelShareBins()) {
|
||||
accountLevelShareBins.add((AccountLevelShareBin) accountLevelShareBin.clone());
|
||||
}
|
||||
|
||||
Map<Integer, List<ExpandedAccount>> accountsForShareBin = new HashMap<>();
|
||||
|
||||
// We might need to combine some share bins if they haven't reached the minimum number of minters yet
|
||||
for (int binIndex = accountLevelShareBins.size()-1; binIndex >= 0; --binIndex) {
|
||||
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
|
||||
// Object reference compare is OK as all references are read-only from blockchain config.
|
||||
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin(this.blockData.getHeight()) == accountLevelShareBin).collect(Collectors.toList());
|
||||
|
||||
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
|
||||
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.hasShareBin(accountLevelShareBin, this.blockData.getHeight())).collect(Collectors.toList());
|
||||
// Add any accounts that have been moved down from a higher tier
|
||||
List<ExpandedAccount> existingBinnedAccounts = accountsForShareBin.get(binIndex);
|
||||
if (existingBinnedAccounts != null)
|
||||
binnedAccounts.addAll(existingBinnedAccounts);
|
||||
|
||||
// Logic below may only apply to higher levels, and only for share bins with a specific range of online accounts
|
||||
if (accountLevelShareBin.levels.get(0) < BlockChain.getInstance().getShareBinActivationMinLevel() ||
|
||||
binnedAccounts.isEmpty() || binnedAccounts.size() >= BlockChain.getInstance().getMinAccountsToActivateShareBin()) {
|
||||
// Add all accounts for this share bin to the accountsForShareBin list
|
||||
accountsForShareBin.put(binIndex, binnedAccounts);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Share bin contains more than one, but less than the minimum number of minters. We treat this share bin
|
||||
// as not activated yet. In these cases, the rewards and minters are combined and paid out to the previous
|
||||
// share bin, to prevent a single or handful of accounts receiving the entire rewards for a share bin.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// - Share bin for levels 5 and 6 has 100 minters
|
||||
// - Share bin for levels 7 and 8 has 10 minters
|
||||
//
|
||||
// This is below the minimum of 30, so share bins are reconstructed as follows:
|
||||
//
|
||||
// - Share bin for levels 5 and 6 now contains 110 minters
|
||||
// - Share bin for levels 7 and 8 now contains 0 minters
|
||||
// - Share bin for levels 5 and 6 now pays out rewards for levels 5, 6, 7, and 8
|
||||
// - Share bin for levels 7 and 8 pays zero rewards
|
||||
//
|
||||
// This process is iterative, so will combine several tiers if needed.
|
||||
|
||||
// Designate this share bin as empty
|
||||
accountsForShareBin.put(binIndex, new ArrayList<>());
|
||||
|
||||
// Move the accounts originally intended for this share bin to the previous one
|
||||
accountsForShareBin.put(binIndex - 1, binnedAccounts);
|
||||
|
||||
// Move the block reward from this share bin to the previous one
|
||||
AccountLevelShareBin previousShareBin = accountLevelShareBins.get(binIndex - 1);
|
||||
previousShareBin.share += accountLevelShareBin.share;
|
||||
accountLevelShareBin.share = 0L;
|
||||
}
|
||||
|
||||
// Now loop through (potentially modified) share bins and determine the reward candidates
|
||||
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
|
||||
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
|
||||
List<ExpandedAccount> binnedAccounts = accountsForShareBin.get(binIndex);
|
||||
|
||||
// No online accounts in this bin? Skip to next one
|
||||
if (binnedAccounts.isEmpty())
|
||||
|
||||
@@ -68,10 +68,12 @@ public class BlockChain {
|
||||
atFindNextTransactionFix,
|
||||
newBlockSigHeight,
|
||||
shareBinFix,
|
||||
rewardShareLimitTimestamp,
|
||||
calcChainWeightTimestamp,
|
||||
transactionV5Timestamp,
|
||||
transactionV6Timestamp,
|
||||
disableReferenceTimestamp
|
||||
disableReferenceTimestamp,
|
||||
aggregateSignatureTimestamp;
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@@ -102,10 +104,23 @@ public class BlockChain {
|
||||
private List<RewardByHeight> rewardsByHeight;
|
||||
|
||||
/** Share of block reward/fees by account level */
|
||||
public static class AccountLevelShareBin {
|
||||
public static class AccountLevelShareBin implements Cloneable {
|
||||
public int id;
|
||||
public List<Integer> levels;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long share;
|
||||
|
||||
public Object clone() {
|
||||
AccountLevelShareBin shareBinCopy = new AccountLevelShareBin();
|
||||
List<Integer> levelsCopy = new ArrayList<>();
|
||||
for (Integer level : this.levels) {
|
||||
levelsCopy.add(level);
|
||||
}
|
||||
shareBinCopy.id = this.id;
|
||||
shareBinCopy.levels = levelsCopy;
|
||||
shareBinCopy.share = this.share;
|
||||
return shareBinCopy;
|
||||
}
|
||||
}
|
||||
private List<AccountLevelShareBin> sharesByLevel;
|
||||
/** Generated lookup of share-bin by account level */
|
||||
@@ -119,6 +134,12 @@ public class BlockChain {
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private Long qoraPerQortReward;
|
||||
|
||||
/** Minimum number of accounts before a share bin is considered activated */
|
||||
private int minAccountsToActivateShareBin;
|
||||
|
||||
/** Min level at which share bin activation takes place; lower levels allow less than minAccountsPerShareBin */
|
||||
private int shareBinActivationMinLevel;
|
||||
|
||||
/**
|
||||
* Number of minted blocks required to reach next level from previous.
|
||||
* <p>
|
||||
@@ -156,7 +177,7 @@ public class BlockChain {
|
||||
private int minAccountLevelToMint;
|
||||
private int minAccountLevelForBlockSubmissions;
|
||||
private int minAccountLevelToRewardShare;
|
||||
private int maxRewardSharesPerMintingAccount;
|
||||
private int maxRewardSharesPerFounderMintingAccount;
|
||||
private int founderEffectiveMintingLevel;
|
||||
|
||||
/** Minimum time to retain online account signatures (ms) for block validity checks. */
|
||||
@@ -164,6 +185,17 @@ public class BlockChain {
|
||||
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
|
||||
private long onlineAccountSignaturesMaxLifetime;
|
||||
|
||||
/** Feature trigger timestamp for ONLINE_ACCOUNTS_MODULUS time interval increase. Can't use
|
||||
* featureTriggers because unit tests need to set this value via Reflection. */
|
||||
private long onlineAccountsModulusV2Timestamp;
|
||||
|
||||
/** Max reward shares by block height */
|
||||
public static class MaxRewardSharesByTimestamp {
|
||||
public long timestamp;
|
||||
public int maxShares;
|
||||
}
|
||||
private List<MaxRewardSharesByTimestamp> maxRewardSharesByTimestamp;
|
||||
|
||||
/** Settings relating to CIYAM AT feature. */
|
||||
public static class CiyamAtSettings {
|
||||
/** Fee per step/op-code executed. */
|
||||
@@ -312,6 +344,11 @@ public class BlockChain {
|
||||
return this.maxBlockSize;
|
||||
}
|
||||
|
||||
// Online accounts
|
||||
public long getOnlineAccountsModulusV2Timestamp() {
|
||||
return this.onlineAccountsModulusV2Timestamp;
|
||||
}
|
||||
|
||||
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
||||
public boolean getRequireGroupForApproval() {
|
||||
return this.requireGroupForApproval;
|
||||
@@ -353,6 +390,14 @@ public class BlockChain {
|
||||
return this.qoraPerQortReward;
|
||||
}
|
||||
|
||||
public int getMinAccountsToActivateShareBin() {
|
||||
return this.minAccountsToActivateShareBin;
|
||||
}
|
||||
|
||||
public int getShareBinActivationMinLevel() {
|
||||
return this.shareBinActivationMinLevel;
|
||||
}
|
||||
|
||||
public int getMinAccountLevelToMint() {
|
||||
return this.minAccountLevelToMint;
|
||||
}
|
||||
@@ -365,8 +410,8 @@ public class BlockChain {
|
||||
return this.minAccountLevelToRewardShare;
|
||||
}
|
||||
|
||||
public int getMaxRewardSharesPerMintingAccount() {
|
||||
return this.maxRewardSharesPerMintingAccount;
|
||||
public int getMaxRewardSharesPerFounderMintingAccount() {
|
||||
return this.maxRewardSharesPerFounderMintingAccount;
|
||||
}
|
||||
|
||||
public int getFounderEffectiveMintingLevel() {
|
||||
@@ -399,6 +444,10 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.shareBinFix.name()).intValue();
|
||||
}
|
||||
|
||||
public long getRewardShareLimitTimestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.rewardShareLimitTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getCalcChainWeightTimestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue();
|
||||
}
|
||||
@@ -415,6 +464,10 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getAggregateSignatureTimestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.aggregateSignatureTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
// More complex getters for aspects that change by height or timestamp
|
||||
|
||||
public long getRewardAtHeight(int ourHeight) {
|
||||
@@ -443,6 +496,14 @@ public class BlockChain {
|
||||
return this.getUnitFee();
|
||||
}
|
||||
|
||||
public int getMaxRewardSharesAtTimestamp(long ourTimestamp) {
|
||||
for (int i = maxRewardSharesByTimestamp.size() - 1; i >= 0; --i)
|
||||
if (maxRewardSharesByTimestamp.get(i).timestamp <= ourTimestamp)
|
||||
return maxRewardSharesByTimestamp.get(i).maxShares;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Validate blockchain config read from JSON */
|
||||
private void validateConfig() {
|
||||
if (this.genesisInfo == null)
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -40,6 +41,7 @@ public class AutoUpdate extends Thread {
|
||||
|
||||
public static final String JAR_FILENAME = "qortal.jar";
|
||||
public static final String NEW_JAR_FILENAME = "new-" + JAR_FILENAME;
|
||||
public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib=";
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(AutoUpdate.class);
|
||||
private static final long CHECK_INTERVAL = 20 * 60 * 1000L; // ms
|
||||
@@ -243,6 +245,11 @@ public class AutoUpdate extends Thread {
|
||||
// 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"));
|
||||
@@ -261,10 +268,19 @@ public class AutoUpdate extends Thread {
|
||||
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"),
|
||||
MessageType.INFO);
|
||||
|
||||
new ProcessBuilder(javaCmd).start();
|
||||
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; // applying update OK
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(String.format("Failed to apply update: %s", e.getMessage()));
|
||||
|
||||
try {
|
||||
|
||||
@@ -65,9 +65,8 @@ public class BlockMinter extends Thread {
|
||||
// Lite nodes do not mint
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Wipe existing unconfirmed transactions
|
||||
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||
|
||||
@@ -77,348 +76,375 @@ public class BlockMinter extends Thread {
|
||||
}
|
||||
|
||||
repository.saveChanges();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Repository issue trying to wipe unconfirmed transactions on start-up: {}", e.getMessage());
|
||||
// Fall-through to normal behaviour in case we can recover
|
||||
}
|
||||
}
|
||||
|
||||
BlockData previousBlockData = null;
|
||||
|
||||
// Vars to keep track of blocks that were skipped due to chain weight
|
||||
byte[] parentSignatureForLastLowWeightBlock = null;
|
||||
Long timeOfLastLowWeightBlock = null;
|
||||
|
||||
List<Block> newBlocks = new ArrayList<>();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Going to need this a lot...
|
||||
BlockRepository blockRepository = repository.getBlockRepository();
|
||||
BlockData previousBlockData = null;
|
||||
|
||||
// Vars to keep track of blocks that were skipped due to chain weight
|
||||
byte[] parentSignatureForLastLowWeightBlock = null;
|
||||
Long timeOfLastLowWeightBlock = null;
|
||||
|
||||
List<Block> newBlocks = new ArrayList<>();
|
||||
|
||||
// Flags for tracking change in whether minting is possible,
|
||||
// so we can notify Controller, and further update SysTray, etc.
|
||||
boolean isMintingPossible = false;
|
||||
boolean wasMintingPossible = isMintingPossible;
|
||||
while (running) {
|
||||
repository.discardChanges(); // Free repository locks, if any
|
||||
|
||||
if (isMintingPossible != wasMintingPossible)
|
||||
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
||||
|
||||
wasMintingPossible = isMintingPossible;
|
||||
|
||||
// Sleep for a while
|
||||
Thread.sleep(1000);
|
||||
|
||||
isMintingPossible = false;
|
||||
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
continue;
|
||||
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
continue;
|
||||
|
||||
// No online accounts? (e.g. during startup)
|
||||
if (OnlineAccountsManager.getInstance().getOnlineAccounts().isEmpty())
|
||||
continue;
|
||||
|
||||
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
||||
// No minting accounts?
|
||||
if (mintingAccountsData.isEmpty())
|
||||
continue;
|
||||
|
||||
// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
|
||||
// Note that minting accounts are actually reward-shares in Qortal
|
||||
Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
|
||||
while (madi.hasNext()) {
|
||||
MintingAccountData mintingAccountData = madi.next();
|
||||
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
||||
if (rewardShareData == null) {
|
||||
// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Optional (non-validated) prevention of block submissions below a defined level.
|
||||
// This is an unvalidated version of Blockchain.minAccountLevelToMint
|
||||
// and exists only to reduce block candidates by default.
|
||||
int level = mintingAccount.getEffectiveMintingLevel();
|
||||
if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Needs a mutable copy of the unmodifiableList
|
||||
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||
BlockData lastBlockData = blockRepository.getLastBlock();
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(Controller.hasMisbehaved);
|
||||
|
||||
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
||||
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
|
||||
if (Synchronizer.getInstance().getRecoveryMode() == false)
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
|
||||
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
continue;
|
||||
|
||||
// If we are stuck on an invalid block, we should allow an alternative to be minted
|
||||
boolean recoverInvalidBlock = false;
|
||||
if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
|
||||
// We've had at least one invalid block
|
||||
long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
|
||||
long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
|
||||
if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
||||
if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
||||
// Last valid block was more than 10 mins ago, but we've had an invalid block since then
|
||||
// Assume that the chain has stalled because there is no alternative valid candidate
|
||||
// Enter recovery mode to allow alternative, valid candidates to be minted
|
||||
recoverInvalidBlock = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
||||
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
||||
continue;
|
||||
|
||||
// There are enough peers with a recent block and our latest block is recent
|
||||
// so go ahead and mint a block if possible.
|
||||
isMintingPossible = true;
|
||||
|
||||
// Check blockchain hasn't changed
|
||||
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
|
||||
previousBlockData = lastBlockData;
|
||||
newBlocks.clear();
|
||||
|
||||
// Reduce log timeout
|
||||
logTimeout = 10 * 1000L;
|
||||
|
||||
// Last low weight block is no longer valid
|
||||
parentSignatureForLastLowWeightBlock = null;
|
||||
}
|
||||
|
||||
// Discard accounts we have already built blocks with
|
||||
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
|
||||
|
||||
// Do we need to build any potential new blocks?
|
||||
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
||||
|
||||
if (parentSignatureForLastLowWeightBlock != null) {
|
||||
// The last iteration found a higher weight block in the network, so sleep for a while
|
||||
// to allow is to sync the higher weight chain. We are sleeping here rather than when
|
||||
// detected as we don't want to hold the blockchain lock open.
|
||||
LOGGER.debug("Sleeping for 10 seconds...");
|
||||
Thread.sleep(10 * 1000L);
|
||||
}
|
||||
|
||||
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
|
||||
// First block does the AT heavy-lifting
|
||||
if (newBlocks.isEmpty()) {
|
||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||
if (newBlock == null) {
|
||||
// For some reason we can't mint right now
|
||||
moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
|
||||
continue;
|
||||
}
|
||||
|
||||
newBlocks.add(newBlock);
|
||||
} else {
|
||||
// The blocks for other minters require less effort...
|
||||
Block newBlock = newBlocks.get(0).remint(mintingAccount);
|
||||
if (newBlock == null) {
|
||||
// For some reason we can't mint right now
|
||||
moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
|
||||
continue;
|
||||
}
|
||||
|
||||
newBlocks.add(newBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// No potential block candidates?
|
||||
if (newBlocks.isEmpty())
|
||||
continue;
|
||||
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
|
||||
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean newBlockMinted = false;
|
||||
Block newBlock = null;
|
||||
|
||||
try {
|
||||
// Clear repository session state so we have latest view of data
|
||||
// Free up any repository locks
|
||||
repository.discardChanges();
|
||||
|
||||
// Now that we have blockchain lock, do final check that chain hasn't changed
|
||||
BlockData latestBlockData = blockRepository.getLastBlock();
|
||||
if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
|
||||
// Sleep for a while
|
||||
Thread.sleep(1000);
|
||||
|
||||
isMintingPossible = false;
|
||||
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
continue;
|
||||
|
||||
List<Block> goodBlocks = new ArrayList<>();
|
||||
for (Block testBlock : newBlocks) {
|
||||
// Is new block's timestamp valid yet?
|
||||
// We do a separate check as some timestamp checks are skipped for testchains
|
||||
if (testBlock.isTimestampValid() != ValidationResult.OK)
|
||||
continue;
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
continue;
|
||||
|
||||
testBlock.preProcess();
|
||||
// No online accounts for current timestamp? (e.g. during startup)
|
||||
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
|
||||
continue;
|
||||
|
||||
// Is new block valid yet? (Before adding unconfirmed transactions)
|
||||
ValidationResult result = testBlock.isValid();
|
||||
if (result != ValidationResult.OK) {
|
||||
moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
|
||||
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
||||
// No minting accounts?
|
||||
if (mintingAccountsData.isEmpty())
|
||||
continue;
|
||||
|
||||
// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
|
||||
// Note that minting accounts are actually reward-shares in Qortal
|
||||
Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
|
||||
while (madi.hasNext()) {
|
||||
MintingAccountData mintingAccountData = madi.next();
|
||||
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
||||
if (rewardShareData == null) {
|
||||
// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
goodBlocks.add(testBlock);
|
||||
}
|
||||
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (goodBlocks.isEmpty())
|
||||
continue;
|
||||
|
||||
// Pick best block
|
||||
final int parentHeight = previousBlockData.getHeight();
|
||||
final byte[] parentBlockSignature = previousBlockData.getSignature();
|
||||
|
||||
BigInteger bestWeight = null;
|
||||
|
||||
for (int bi = 0; bi < goodBlocks.size(); ++bi) {
|
||||
BlockData blockData = goodBlocks.get(bi).getBlockData();
|
||||
|
||||
BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
|
||||
blockSummaryData.setMinterLevel(minterLevel);
|
||||
|
||||
BigInteger blockWeight = Block.calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
|
||||
|
||||
if (bestWeight == null || blockWeight.compareTo(bestWeight) < 0) {
|
||||
newBlock = goodBlocks.get(bi);
|
||||
bestWeight = blockWeight;
|
||||
// Optional (non-validated) prevention of block submissions below a defined level.
|
||||
// This is an unvalidated version of Blockchain.minAccountLevelToMint
|
||||
// and exists only to reduce block candidates by default.
|
||||
int level = mintingAccount.getEffectiveMintingLevel();
|
||||
if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.higherWeightChainExists(repository, bestWeight)) {
|
||||
// Needs a mutable copy of the unmodifiableList
|
||||
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||
BlockData lastBlockData = blockRepository.getLastBlock();
|
||||
|
||||
// Check if the base block has updated since the last time we were here
|
||||
if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
|
||||
!Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
|
||||
// We've switched to a different chain, so reset the timer
|
||||
timeOfLastLowWeightBlock = NTP.getTime();
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(Controller.hasMisbehaved);
|
||||
|
||||
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
||||
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
|
||||
if (Synchronizer.getInstance().getRecoveryMode() == false)
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
|
||||
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
continue;
|
||||
|
||||
// If we are stuck on an invalid block, we should allow an alternative to be minted
|
||||
boolean recoverInvalidBlock = false;
|
||||
if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
|
||||
// We've had at least one invalid block
|
||||
long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
|
||||
long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
|
||||
if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
||||
if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
||||
// Last valid block was more than 10 mins ago, but we've had an invalid block since then
|
||||
// Assume that the chain has stalled because there is no alternative valid candidate
|
||||
// Enter recovery mode to allow alternative, valid candidates to be minted
|
||||
recoverInvalidBlock = true;
|
||||
}
|
||||
parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
|
||||
}
|
||||
}
|
||||
|
||||
// If less than 30 seconds has passed since first detection the higher weight chain,
|
||||
// we should skip our block submission to give us the opportunity to sync to the better chain
|
||||
if (NTP.getTime() - timeOfLastLowWeightBlock < 30*1000L) {
|
||||
LOGGER.debug("Higher weight chain found in peers, so not signing a block this round");
|
||||
LOGGER.debug("Time since detected: {}ms", NTP.getTime() - timeOfLastLowWeightBlock);
|
||||
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
||||
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
||||
continue;
|
||||
|
||||
// There are enough peers with a recent block and our latest block is recent
|
||||
// so go ahead and mint a block if possible.
|
||||
isMintingPossible = true;
|
||||
|
||||
// Check blockchain hasn't changed
|
||||
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
|
||||
previousBlockData = lastBlockData;
|
||||
newBlocks.clear();
|
||||
|
||||
// Reduce log timeout
|
||||
logTimeout = 10 * 1000L;
|
||||
|
||||
// Last low weight block is no longer valid
|
||||
parentSignatureForLastLowWeightBlock = null;
|
||||
}
|
||||
|
||||
// Discard accounts we have already built blocks with
|
||||
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
|
||||
|
||||
// Do we need to build any potential new blocks?
|
||||
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
||||
|
||||
// We might need to sit the next block out, if one of our minting accounts signed the previous one
|
||||
byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
|
||||
boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
|
||||
if (mintedLastBlock) {
|
||||
LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parentSignatureForLastLowWeightBlock != null) {
|
||||
// The last iteration found a higher weight block in the network, so sleep for a while
|
||||
// to allow is to sync the higher weight chain. We are sleeping here rather than when
|
||||
// detected as we don't want to hold the blockchain lock open.
|
||||
LOGGER.info("Sleeping for 10 seconds...");
|
||||
Thread.sleep(10 * 1000L);
|
||||
}
|
||||
|
||||
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
|
||||
// First block does the AT heavy-lifting
|
||||
if (newBlocks.isEmpty()) {
|
||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||
if (newBlock == null) {
|
||||
// For some reason we can't mint right now
|
||||
moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
// More than 30 seconds have passed, so we should submit our block candidate anyway.
|
||||
LOGGER.debug("More than 30 seconds passed, so proceeding to submit block candidate...");
|
||||
|
||||
newBlocks.add(newBlock);
|
||||
} else {
|
||||
// The blocks for other minters require less effort...
|
||||
Block newBlock = newBlocks.get(0).remint(mintingAccount);
|
||||
if (newBlock == null) {
|
||||
// For some reason we can't mint right now
|
||||
moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
|
||||
continue;
|
||||
}
|
||||
|
||||
newBlocks.add(newBlock);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("No higher weight chain found in peers");
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
|
||||
}
|
||||
|
||||
// Discard any uncommitted changes as a result of the higher weight chain detection
|
||||
repository.discardChanges();
|
||||
// No potential block candidates?
|
||||
if (newBlocks.isEmpty())
|
||||
continue;
|
||||
|
||||
// Clear variables that track low weight blocks
|
||||
parentSignatureForLastLowWeightBlock = null;
|
||||
timeOfLastLowWeightBlock = null;
|
||||
|
||||
|
||||
// Add unconfirmed transactions
|
||||
addUnconfirmedTransactions(repository, newBlock);
|
||||
|
||||
// Sign to create block's signature
|
||||
newBlock.sign();
|
||||
|
||||
// Is newBlock still valid?
|
||||
ValidationResult validationResult = newBlock.isValid();
|
||||
if (validationResult != ValidationResult.OK) {
|
||||
// No longer valid? Report and discard
|
||||
LOGGER.error(String.format("To-be-minted block now invalid '%s' after adding unconfirmed transactions?", validationResult.name()));
|
||||
|
||||
// Rebuild block candidates, just to be sure
|
||||
newBlocks.clear();
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
|
||||
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to blockchain - something else will notice and broadcast new block to network
|
||||
boolean newBlockMinted = false;
|
||||
Block newBlock = null;
|
||||
|
||||
try {
|
||||
newBlock.process();
|
||||
// Clear repository session state so we have latest view of data
|
||||
repository.discardChanges();
|
||||
|
||||
repository.saveChanges();
|
||||
// Now that we have blockchain lock, do final check that chain hasn't changed
|
||||
BlockData latestBlockData = blockRepository.getLastBlock();
|
||||
if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
|
||||
continue;
|
||||
|
||||
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
|
||||
List<Block> goodBlocks = new ArrayList<>();
|
||||
boolean wasInvalidBlockDiscarded = false;
|
||||
Iterator<Block> newBlocksIterator = newBlocks.iterator();
|
||||
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
|
||||
while (newBlocksIterator.hasNext()) {
|
||||
Block testBlock = newBlocksIterator.next();
|
||||
|
||||
if (rewardShareData != null) {
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
|
||||
newBlock.getBlockData().getHeight(),
|
||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||
Base58.encode(newBlock.getParent().getSignature()),
|
||||
rewardShareData.getMinter(),
|
||||
rewardShareData.getRecipient()));
|
||||
} else {
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
|
||||
newBlock.getBlockData().getHeight(),
|
||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||
Base58.encode(newBlock.getParent().getSignature()),
|
||||
newBlock.getMinter().getAddress()));
|
||||
// Is new block's timestamp valid yet?
|
||||
// We do a separate check as some timestamp checks are skipped for testchains
|
||||
if (testBlock.isTimestampValid() != ValidationResult.OK)
|
||||
continue;
|
||||
|
||||
testBlock.preProcess();
|
||||
|
||||
// Is new block valid yet? (Before adding unconfirmed transactions)
|
||||
ValidationResult result = testBlock.isValid();
|
||||
if (result != ValidationResult.OK) {
|
||||
moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
|
||||
|
||||
newBlocksIterator.remove();
|
||||
wasInvalidBlockDiscarded = true;
|
||||
/*
|
||||
* Bail out fast so that we loop around from the top again.
|
||||
* This gives BlockMinter the possibility to remint this candidate block using another block from newBlocks,
|
||||
* via the Blocks.remint() method, which avoids having to re-process Block ATs all over again.
|
||||
* Particularly useful if some aspect of Blocks changes due a timestamp-based feature-trigger (see BlockChain class).
|
||||
*/
|
||||
break;
|
||||
}
|
||||
|
||||
goodBlocks.add(testBlock);
|
||||
}
|
||||
|
||||
// Notify network after we're released blockchain lock
|
||||
newBlockMinted = true;
|
||||
if (wasInvalidBlockDiscarded || goodBlocks.isEmpty())
|
||||
continue;
|
||||
|
||||
// Notify Controller
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
} catch (DataException e) {
|
||||
// Unable to process block - report and discard
|
||||
LOGGER.error("Unable to process newly minted block?", e);
|
||||
newBlocks.clear();
|
||||
// Pick best block
|
||||
final int parentHeight = previousBlockData.getHeight();
|
||||
final byte[] parentBlockSignature = previousBlockData.getSignature();
|
||||
|
||||
BigInteger bestWeight = null;
|
||||
|
||||
for (int bi = 0; bi < goodBlocks.size(); ++bi) {
|
||||
BlockData blockData = goodBlocks.get(bi).getBlockData();
|
||||
|
||||
BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
|
||||
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
|
||||
blockSummaryData.setMinterLevel(minterLevel);
|
||||
|
||||
BigInteger blockWeight = Block.calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
|
||||
|
||||
if (bestWeight == null || blockWeight.compareTo(bestWeight) < 0) {
|
||||
newBlock = goodBlocks.get(bi);
|
||||
bestWeight = blockWeight;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.higherWeightChainExists(repository, bestWeight)) {
|
||||
|
||||
// Check if the base block has updated since the last time we were here
|
||||
if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
|
||||
!Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
|
||||
// We've switched to a different chain, so reset the timer
|
||||
timeOfLastLowWeightBlock = NTP.getTime();
|
||||
}
|
||||
parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
|
||||
|
||||
// If less than 30 seconds has passed since first detection the higher weight chain,
|
||||
// we should skip our block submission to give us the opportunity to sync to the better chain
|
||||
if (NTP.getTime() - timeOfLastLowWeightBlock < 30 * 1000L) {
|
||||
LOGGER.info("Higher weight chain found in peers, so not signing a block this round");
|
||||
LOGGER.info("Time since detected: {}", NTP.getTime() - timeOfLastLowWeightBlock);
|
||||
continue;
|
||||
} else {
|
||||
// More than 30 seconds have passed, so we should submit our block candidate anyway.
|
||||
LOGGER.info("More than 30 seconds passed, so proceeding to submit block candidate...");
|
||||
}
|
||||
} else {
|
||||
LOGGER.debug("No higher weight chain found in peers");
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
|
||||
}
|
||||
|
||||
// Discard any uncommitted changes as a result of the higher weight chain detection
|
||||
repository.discardChanges();
|
||||
|
||||
// Clear variables that track low weight blocks
|
||||
parentSignatureForLastLowWeightBlock = null;
|
||||
timeOfLastLowWeightBlock = null;
|
||||
|
||||
// Add unconfirmed transactions
|
||||
addUnconfirmedTransactions(repository, newBlock);
|
||||
|
||||
// Sign to create block's signature
|
||||
newBlock.sign();
|
||||
|
||||
// Is newBlock still valid?
|
||||
ValidationResult validationResult = newBlock.isValid();
|
||||
if (validationResult != ValidationResult.OK) {
|
||||
// No longer valid? Report and discard
|
||||
LOGGER.error(String.format("To-be-minted block now invalid '%s' after adding unconfirmed transactions?", validationResult.name()));
|
||||
|
||||
// Rebuild block candidates, just to be sure
|
||||
newBlocks.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to blockchain - something else will notice and broadcast new block to network
|
||||
try {
|
||||
newBlock.process();
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
|
||||
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
|
||||
|
||||
if (rewardShareData != null) {
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
|
||||
newBlock.getBlockData().getHeight(),
|
||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||
Base58.encode(newBlock.getParent().getSignature()),
|
||||
rewardShareData.getMinter(),
|
||||
rewardShareData.getRecipient()));
|
||||
} else {
|
||||
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
|
||||
newBlock.getBlockData().getHeight(),
|
||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
||||
Base58.encode(newBlock.getParent().getSignature()),
|
||||
newBlock.getMinter().getAddress()));
|
||||
}
|
||||
|
||||
// Notify network after we're released blockchain lock
|
||||
newBlockMinted = true;
|
||||
|
||||
// Notify Controller
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
} catch (DataException e) {
|
||||
// Unable to process block - report and discard
|
||||
LOGGER.error("Unable to process newly minted block?", e);
|
||||
newBlocks.clear();
|
||||
}
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
|
||||
if (newBlockMinted) {
|
||||
// Broadcast our new chain to network
|
||||
BlockData newBlockData = newBlock.getBlockData();
|
||||
if (newBlockMinted) {
|
||||
// Broadcast our new chain to network
|
||||
BlockData newBlockData = newBlock.getBlockData();
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We've been interrupted - time to exit
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Repository issue while running block minter", e);
|
||||
} catch (InterruptedException e) {
|
||||
// We've been interrupted - time to exit
|
||||
return;
|
||||
LOGGER.warn("Repository issue while running block minter - NO LONGER MINTING", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,18 +575,23 @@ public class BlockMinter extends Thread {
|
||||
// This peer has common block data
|
||||
CommonBlockData commonBlockData = peer.getCommonBlockData();
|
||||
BlockSummaryData commonBlockSummaryData = commonBlockData.getCommonBlockSummary();
|
||||
if (commonBlockData.getChainWeight() != null) {
|
||||
if (commonBlockData.getChainWeight() != null && peer.getCommonBlockData().getBlockSummariesAfterCommonBlock() != null) {
|
||||
// The synchronizer has calculated this peer's chain weight
|
||||
BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
|
||||
BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
|
||||
BigInteger peerChainWeight = commonBlockData.getChainWeight();
|
||||
if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
|
||||
// This peer has a higher weight chain than ours
|
||||
LOGGER.debug("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
|
||||
return true;
|
||||
if (!Synchronizer.getInstance().containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
|
||||
// .. and it doesn't hold any invalid blocks
|
||||
BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
|
||||
BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
|
||||
BigInteger peerChainWeight = commonBlockData.getChainWeight();
|
||||
if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
|
||||
// This peer has a higher weight chain than ours
|
||||
LOGGER.info("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
|
||||
return true;
|
||||
|
||||
} else {
|
||||
LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
|
||||
}
|
||||
} else {
|
||||
LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
|
||||
LOGGER.debug("Peer {} has an invalid block", peer);
|
||||
}
|
||||
} else {
|
||||
LOGGER.debug("Peer {} has no chain weight", peer);
|
||||
|
||||
@@ -113,6 +113,7 @@ public class Controller extends Thread {
|
||||
private long repositoryBackupTimestamp = startTime; // ms
|
||||
private long repositoryMaintenanceTimestamp = startTime; // ms
|
||||
private long repositoryCheckpointTimestamp = startTime; // ms
|
||||
private long prunePeersTimestamp = startTime; // ms
|
||||
private long ntpCheckTimestamp = startTime; // ms
|
||||
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
|
||||
|
||||
@@ -496,6 +497,9 @@ public class Controller extends Thread {
|
||||
AutoUpdate.getInstance().start();
|
||||
}
|
||||
|
||||
LOGGER.info("Starting wallets");
|
||||
PirateChainWalletController.getInstance().start();
|
||||
|
||||
LOGGER.info(String.format("Starting API on port %d", Settings.getInstance().getApiPort()));
|
||||
try {
|
||||
ApiService apiService = ApiService.getInstance();
|
||||
@@ -552,6 +556,7 @@ public class Controller extends Thread {
|
||||
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
||||
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
|
||||
long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval();
|
||||
final long prunePeersInterval = 5 * 60 * 1000L; // Every 5 minutes
|
||||
|
||||
// Start executor service for trimming or pruning
|
||||
PruneManager.getInstance().start();
|
||||
@@ -649,10 +654,15 @@ public class Controller extends Thread {
|
||||
}
|
||||
|
||||
// Prune stuck/slow/old peers
|
||||
try {
|
||||
Network.getInstance().prunePeers();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
|
||||
if (now >= prunePeersTimestamp + prunePeersInterval) {
|
||||
prunePeersTimestamp = now + prunePeersInterval;
|
||||
|
||||
try {
|
||||
LOGGER.debug("Pruning peers...");
|
||||
Network.getInstance().prunePeers();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// Delete expired transactions
|
||||
@@ -787,23 +797,24 @@ public class Controller extends Thread {
|
||||
String actionText;
|
||||
|
||||
// Use a more tolerant latest block timestamp in the isUpToDate() calls below to reduce misleading statuses.
|
||||
// Any block in the last 30 minutes is considered "up to date" for the purposes of displaying statuses.
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
|
||||
// Any block in the last 2 hours is considered "up to date" for the purposes of displaying statuses.
|
||||
// This also aligns with the time interval required for continued online account submission.
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (2 * 60 * 60 * 1000L);
|
||||
|
||||
// Only show sync percent if it's less than 100, to avoid confusion
|
||||
final Integer syncPercent = Synchronizer.getInstance().getSyncPercent();
|
||||
final boolean isSyncing = (syncPercent != null && syncPercent < 100);
|
||||
|
||||
synchronized (Synchronizer.getInstance().syncLock) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "LITE_NODE");
|
||||
SysTray.getInstance().setTrayIcon(4);
|
||||
}
|
||||
else if (this.isMintingPossible) {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||
SysTray.getInstance().setTrayIcon(2);
|
||||
}
|
||||
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers()) {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
|
||||
SysTray.getInstance().setTrayIcon(3);
|
||||
}
|
||||
else if (!this.isUpToDate(minLatestBlockTimestamp) && Synchronizer.getInstance().isSynchronizing()) {
|
||||
else if (!this.isUpToDate(minLatestBlockTimestamp) && isSyncing) {
|
||||
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), Synchronizer.getInstance().getSyncPercent());
|
||||
SysTray.getInstance().setTrayIcon(3);
|
||||
}
|
||||
@@ -811,6 +822,10 @@ public class Controller extends Thread {
|
||||
actionText = String.format("%s", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"));
|
||||
SysTray.getInstance().setTrayIcon(3);
|
||||
}
|
||||
else if (OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures()) {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||
SysTray.getInstance().setTrayIcon(2);
|
||||
}
|
||||
else {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
|
||||
SysTray.getInstance().setTrayIcon(4);
|
||||
@@ -878,6 +893,9 @@ public class Controller extends Thread {
|
||||
LOGGER.info("Shutting down API");
|
||||
ApiService.getInstance().stop();
|
||||
|
||||
LOGGER.info("Shutting down wallets");
|
||||
PirateChainWalletController.getInstance().shutdown();
|
||||
|
||||
if (Settings.getInstance().isAutoUpdateEnabled()) {
|
||||
LOGGER.info("Shutting down auto-update");
|
||||
AutoUpdate.getInstance().shutdown();
|
||||
@@ -1229,6 +1247,10 @@ public class Controller extends Thread {
|
||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV2Message(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ONLINE_ACCOUNTS_V3:
|
||||
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ARBITRARY_DATA:
|
||||
// Not currently supported
|
||||
break;
|
||||
@@ -1362,6 +1384,18 @@ public class Controller extends Thread {
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
|
||||
// V2 support
|
||||
if (peer.getPeersVersion() >= BlockV2Message.MIN_PEER_VERSION) {
|
||||
Message blockMessage = new BlockV2Message(block);
|
||||
blockMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(blockMessage)) {
|
||||
peer.disconnect("failed to send block");
|
||||
// Don't fall-through to caching because failure to send might be from failure to build message
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
CachedBlockMessage blockMessage = new CachedBlockMessage(block);
|
||||
blockMessage.setId(message.getId());
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ public class LiteNode {
|
||||
}
|
||||
|
||||
if (responseMessage == null) {
|
||||
LOGGER.info("Peer didn't respond to {} message", peer, message.getType());
|
||||
LOGGER.info("Peer {} didn't respond to {} message", peer, message.getType());
|
||||
return null;
|
||||
}
|
||||
else if (responseMessage.getType() != expectedResponseMessageType) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import com.rust.litewalletjni.LiteWalletJni;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.PirateWallet;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.ArbitraryTransaction;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.FilesystemUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class PirateChainWalletController extends Thread {
|
||||
|
||||
protected static final Logger LOGGER = LogManager.getLogger(PirateChainWalletController.class);
|
||||
|
||||
private static PirateChainWalletController instance;
|
||||
|
||||
final private static long SAVE_INTERVAL = 60 * 60 * 1000L; // 1 hour
|
||||
private long lastSaveTime = 0L;
|
||||
|
||||
private boolean running;
|
||||
private PirateWallet currentWallet = null;
|
||||
private boolean shouldLoadWallet = false;
|
||||
private String loadStatus = null;
|
||||
|
||||
private static String qdnWalletSignature = "EsfUw54perxkEtfoUoL7Z97XPrNsZRZXePVZPz3cwRm9qyEPSofD5KmgVpDqVitQp7LhnZRmL6z2V9hEe1YS45T";
|
||||
|
||||
|
||||
private PirateChainWalletController() {
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
public static PirateChainWalletController getInstance() {
|
||||
if (instance == null)
|
||||
instance = new PirateChainWalletController();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Pirate Chain Wallet Controller");
|
||||
|
||||
try {
|
||||
while (running && !Controller.isStopping()) {
|
||||
Thread.sleep(1000);
|
||||
|
||||
// Wait until we have a request to load the wallet
|
||||
if (!shouldLoadWallet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!LiteWalletJni.isLoaded()) {
|
||||
this.loadLibrary();
|
||||
|
||||
// If still not loaded, sleep to prevent too many requests
|
||||
if (!LiteWalletJni.isLoaded()) {
|
||||
Thread.sleep(5 * 1000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Wallet is downloaded, so clear the status
|
||||
this.loadStatus = null;
|
||||
|
||||
if (this.currentWallet == null) {
|
||||
// Nothing to do yet
|
||||
continue;
|
||||
}
|
||||
if (this.currentWallet.isNullSeedWallet()) {
|
||||
// Don't sync the null seed wallet
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.debug("Syncing Pirate Chain wallet...");
|
||||
String response = LiteWalletJni.execute("sync", "");
|
||||
LOGGER.debug("sync response: {}", response);
|
||||
JSONObject json = new JSONObject(response);
|
||||
if (json.has("result")) {
|
||||
String result = json.getString("result");
|
||||
|
||||
// We may have to set wallet to ready if this is the first ever successful sync
|
||||
if (Objects.equals(result, "success")) {
|
||||
this.currentWallet.setReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limit sync attempts
|
||||
Thread.sleep(30000);
|
||||
|
||||
// Save wallet if needed
|
||||
Long now = NTP.getTime();
|
||||
if (now != null && now-SAVE_INTERVAL >= this.lastSaveTime) {
|
||||
this.saveCurrentWallet();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall-through to exit
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
// Save the wallet
|
||||
this.saveCurrentWallet();
|
||||
|
||||
this.running = false;
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
|
||||
// QDN & wallet libraries
|
||||
|
||||
private void loadLibrary() throws InterruptedException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Check if architecture is supported
|
||||
String libFileName = PirateChainWalletController.getRustLibFilename();
|
||||
if (libFileName == null) {
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArchitecture = System.getProperty("os.arch");
|
||||
this.loadStatus = String.format("Unsupported architecture (%s %s)", osName, osArchitecture);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the library exists in the wallets folder
|
||||
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
|
||||
Path libPath = Paths.get(libDirectory.toString(), libFileName);
|
||||
if (Files.exists(libPath)) {
|
||||
// Already downloaded; we can load the library right away
|
||||
LiteWalletJni.loadLibrary();
|
||||
return;
|
||||
}
|
||||
|
||||
// Library not found, so check if we've fetched the resource from QDN
|
||||
ArbitraryTransactionData t = this.getTransactionData(repository);
|
||||
if (t == null) {
|
||||
// Can't find the transaction - maybe on a different chain?
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until we have a sufficient number of peers to attempt QDN downloads
|
||||
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
if (handshakedPeers.size() < Settings.getInstance().getMinBlockchainPeers()) {
|
||||
// Wait for more peers
|
||||
this.loadStatus = String.format("Searching for peers...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build resource
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(t.getName(),
|
||||
ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier());
|
||||
try {
|
||||
arbitraryDataReader.loadSynchronously(false);
|
||||
} catch (MissingDataException e) {
|
||||
LOGGER.info("Missing data when loading Pirate Chain library");
|
||||
}
|
||||
|
||||
// Check its status
|
||||
ArbitraryResourceStatus status = ArbitraryTransactionUtils.getStatus(
|
||||
t.getService(), t.getName(), t.getIdentifier(), false);
|
||||
|
||||
if (status.getStatus() != ArbitraryResourceStatus.Status.READY) {
|
||||
LOGGER.info("Not ready yet: {}", status.getTitle());
|
||||
this.loadStatus = String.format("Downloading files from QDN... (%d / %d)", status.getLocalChunkCount(), status.getTotalChunkCount());
|
||||
return;
|
||||
}
|
||||
|
||||
// Files are downloaded, so copy the necessary files to the wallets folder
|
||||
// Delete the wallets/*/lib directory first, in case earlier versions of the wallet are present
|
||||
Path walletsLibDirectory = PirateChainWalletController.getWalletsLibDirectory();
|
||||
if (Files.exists(walletsLibDirectory)) {
|
||||
FilesystemUtils.safeDeleteDirectory(walletsLibDirectory, false);
|
||||
}
|
||||
Files.createDirectories(libDirectory);
|
||||
FileUtils.copyDirectory(arbitraryDataReader.getFilePath().toFile(), libDirectory.toFile());
|
||||
|
||||
// Clear reader cache so only one copy exists
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(t.getName(),
|
||||
ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier());
|
||||
resource.deleteCache();
|
||||
|
||||
// Finally, load the library
|
||||
LiteWalletJni.loadLibrary();
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when loading Pirate Chain library", e);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Error when loading Pirate Chain library", e);
|
||||
}
|
||||
}
|
||||
|
||||
private ArbitraryTransactionData getTransactionData(Repository repository) {
|
||||
try {
|
||||
byte[] signature = Base58.decode(qdnWalletSignature);
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (!(transactionData instanceof ArbitraryTransactionData))
|
||||
return null;
|
||||
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
if (arbitraryTransaction != null) {
|
||||
return (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (DataException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getRustLibFilename() {
|
||||
String osName = System.getProperty("os.name");
|
||||
String osArchitecture = System.getProperty("os.arch");
|
||||
|
||||
if (osName.equals("Mac OS X") && osArchitecture.equals("x86_64")) {
|
||||
return "librust-macos-x86_64.dylib";
|
||||
}
|
||||
else if (osName.equals("Linux") && osArchitecture.equals("aarch64")) {
|
||||
return "librust-linux-aarch64.so";
|
||||
}
|
||||
else if (osName.equals("Linux") && osArchitecture.equals("amd64")) {
|
||||
return "librust-linux-x86_64.so";
|
||||
}
|
||||
else if (osName.contains("Windows") && osArchitecture.equals("amd64")) {
|
||||
return "librust-windows-x86_64.dll";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Path getWalletsLibDirectory() {
|
||||
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib");
|
||||
}
|
||||
|
||||
public static Path getRustLibOuterDirectory() {
|
||||
String sigPrefix = qdnWalletSignature.substring(0, 8);
|
||||
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib", sigPrefix);
|
||||
}
|
||||
|
||||
|
||||
// Wallet functions
|
||||
|
||||
public boolean initWithEntropy58(String entropy58) {
|
||||
return this.initWithEntropy58(entropy58, false);
|
||||
}
|
||||
|
||||
public boolean initNullSeedWallet() {
|
||||
return this.initWithEntropy58(Base58.encode(new byte[32]), true);
|
||||
}
|
||||
|
||||
private boolean initWithEntropy58(String entropy58, boolean isNullSeedWallet) {
|
||||
// If the JNI library isn't loaded yet then we can't proceed
|
||||
if (!LiteWalletJni.isLoaded()) {
|
||||
shouldLoadWallet = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] entropyBytes = Base58.decode(entropy58);
|
||||
|
||||
if (entropyBytes == null || entropyBytes.length != 32) {
|
||||
LOGGER.info("Invalid entropy bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.currentWallet != null) {
|
||||
if (this.currentWallet.entropyBytesEqual(entropyBytes)) {
|
||||
// Wallet already active - nothing to do
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// Different wallet requested - close the existing one and switch over
|
||||
this.closeCurrentWallet();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.currentWallet = new PirateWallet(entropyBytes, isNullSeedWallet);
|
||||
if (!this.currentWallet.isReady()) {
|
||||
// Don't persist wallets that aren't ready
|
||||
this.currentWallet = null;
|
||||
}
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to initialize wallet: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void saveCurrentWallet() {
|
||||
if (this.currentWallet == null) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (this.currentWallet.save()) {
|
||||
Long now = NTP.getTime();
|
||||
if (now != null) {
|
||||
this.lastSaveTime = now;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to save wallet");
|
||||
}
|
||||
}
|
||||
|
||||
public PirateWallet getCurrentWallet() {
|
||||
return this.currentWallet;
|
||||
}
|
||||
|
||||
private void closeCurrentWallet() {
|
||||
this.saveCurrentWallet();
|
||||
this.currentWallet = null;
|
||||
}
|
||||
|
||||
public void ensureInitialized() throws ForeignBlockchainException {
|
||||
if (!LiteWalletJni.isLoaded() || this.currentWallet == null || !this.currentWallet.isInitialized()) {
|
||||
throw new ForeignBlockchainException("Pirate wallet isn't initialized yet");
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureNotNullSeed() throws ForeignBlockchainException {
|
||||
// Safety check to make sure funds aren't sent to a null seed wallet
|
||||
if (this.currentWallet == null || this.currentWallet.isNullSeedWallet()) {
|
||||
throw new ForeignBlockchainException("Invalid wallet");
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureSynchronized() throws ForeignBlockchainException {
|
||||
if (this.currentWallet == null || !this.currentWallet.isSynchronized()) {
|
||||
throw new ForeignBlockchainException("Wallet isn't synchronized yet");
|
||||
}
|
||||
|
||||
String response = LiteWalletJni.execute("syncStatus", "");
|
||||
JSONObject json = new JSONObject(response);
|
||||
if (json.has("syncing")) {
|
||||
boolean isSyncing = Boolean.valueOf(json.getString("syncing"));
|
||||
if (isSyncing) {
|
||||
long syncedBlocks = json.getLong("synced_blocks");
|
||||
long totalBlocks = json.getLong("total_blocks");
|
||||
|
||||
throw new ForeignBlockchainException(String.format("Sync in progress (%d / %d). Please try again later.", syncedBlocks, totalBlocks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getSyncStatus() {
|
||||
if (this.currentWallet == null || !this.currentWallet.isInitialized()) {
|
||||
if (this.loadStatus != null) {
|
||||
return this.loadStatus;
|
||||
}
|
||||
|
||||
return "Not initialized yet";
|
||||
}
|
||||
|
||||
String syncStatusResponse = LiteWalletJni.execute("syncStatus", "");
|
||||
org.json.JSONObject json = new JSONObject(syncStatusResponse);
|
||||
if (json.has("syncing")) {
|
||||
boolean isSyncing = Boolean.valueOf(json.getString("syncing"));
|
||||
if (isSyncing) {
|
||||
long syncedBlocks = json.getLong("synced_blocks");
|
||||
long totalBlocks = json.getLong("total_blocks");
|
||||
return String.format("Sync in progress (%d / %d)", syncedBlocks, totalBlocks);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSynchronized = this.currentWallet.isSynchronized();
|
||||
if (isSynchronized) {
|
||||
return "Synchronized";
|
||||
}
|
||||
|
||||
return "Initializing wallet...";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,14 +26,7 @@ import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.BlockMessage;
|
||||
import org.qortal.network.message.BlockSummariesMessage;
|
||||
import org.qortal.network.message.GetBlockMessage;
|
||||
import org.qortal.network.message.GetBlockSummariesMessage;
|
||||
import org.qortal.network.message.GetSignaturesV2Message;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.SignaturesMessage;
|
||||
import org.qortal.network.message.MessageType;
|
||||
import org.qortal.network.message.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -88,7 +81,7 @@ public class Synchronizer extends Thread {
|
||||
private boolean syncRequestPending = false;
|
||||
|
||||
// Keep track of invalid blocks so that we don't keep trying to sync them
|
||||
private Map<String, Long> invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
|
||||
private Map<ByteArray, Long> invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
|
||||
public Long timeValidBlockLastReceived = null;
|
||||
public Long timeInvalidBlockLastReceived = null;
|
||||
|
||||
@@ -178,8 +171,8 @@ public class Synchronizer extends Thread {
|
||||
|
||||
public Integer getSyncPercent() {
|
||||
synchronized (this.syncLock) {
|
||||
// Report as 100% synced if the latest block is within the last 30 mins
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
|
||||
// Report as 100% synced if the latest block is within the last 60 mins
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
if (Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
|
||||
return 100;
|
||||
}
|
||||
@@ -624,7 +617,7 @@ public class Synchronizer extends Thread {
|
||||
// We have already determined that the correct chain diverged from a lower height. We are safe to skip these peers.
|
||||
for (Peer peer : peersSharingCommonBlock) {
|
||||
LOGGER.debug(String.format("Peer %s has common block at height %d but the superior chain is at height %d. Removing it from this round.", peer, commonBlockSummary.getHeight(), dropPeersAfterCommonBlockHeight));
|
||||
this.addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
|
||||
//this.addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -635,7 +628,9 @@ public class Synchronizer extends Thread {
|
||||
int minChainLength = this.calculateMinChainLengthOfPeers(peersSharingCommonBlock, commonBlockSummary);
|
||||
|
||||
// Fetch block summaries from each peer
|
||||
for (Peer peer : peersSharingCommonBlock) {
|
||||
Iterator peersSharingCommonBlockIterator = peersSharingCommonBlock.iterator();
|
||||
while (peersSharingCommonBlockIterator.hasNext()) {
|
||||
Peer peer = (Peer) peersSharingCommonBlockIterator.next();
|
||||
|
||||
// If we're shutting down, just return the latest peer list
|
||||
if (Controller.isStopping())
|
||||
@@ -692,6 +687,8 @@ public class Synchronizer extends Thread {
|
||||
if (this.containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
|
||||
LOGGER.debug("Ignoring peer %s because it holds an invalid block", peer);
|
||||
peers.remove(peer);
|
||||
peersSharingCommonBlockIterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reduce minChainLength if needed. If we don't have any blocks, this peer will be excluded from chain weight comparisons later in the process, so we shouldn't update minChainLength
|
||||
@@ -847,6 +844,10 @@ public class Synchronizer extends Thread {
|
||||
|
||||
/* Invalid block signature tracking */
|
||||
|
||||
public Map<ByteArray, Long> getInvalidBlockSignatures() {
|
||||
return this.invalidBlockSignatures;
|
||||
}
|
||||
|
||||
private void addInvalidBlockSignature(byte[] signature) {
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
@@ -854,8 +855,7 @@ public class Synchronizer extends Thread {
|
||||
}
|
||||
|
||||
// Add or update existing entry
|
||||
String sig58 = Base58.encode(signature);
|
||||
invalidBlockSignatures.put(sig58, now);
|
||||
invalidBlockSignatures.put(ByteArray.wrap(signature), now);
|
||||
}
|
||||
private void deleteOlderInvalidSignatures(Long now) {
|
||||
if (now == null) {
|
||||
@@ -874,17 +874,16 @@ public class Synchronizer extends Thread {
|
||||
}
|
||||
}
|
||||
}
|
||||
private boolean containsInvalidBlockSummary(List<BlockSummaryData> blockSummaries) {
|
||||
public boolean containsInvalidBlockSummary(List<BlockSummaryData> blockSummaries) {
|
||||
if (blockSummaries == null || invalidBlockSignatures == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loop through our known invalid blocks and check each one against supplied block summaries
|
||||
for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
|
||||
byte[] invalidSignature = Base58.decode(invalidSignature58);
|
||||
for (ByteArray invalidSignature : invalidBlockSignatures.keySet()) {
|
||||
for (BlockSummaryData blockSummary : blockSummaries) {
|
||||
byte[] signature = blockSummary.getSignature();
|
||||
if (Arrays.equals(signature, invalidSignature)) {
|
||||
if (Arrays.equals(signature, invalidSignature.value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -897,10 +896,9 @@ public class Synchronizer extends Thread {
|
||||
}
|
||||
|
||||
// Loop through our known invalid blocks and check each one against supplied block signatures
|
||||
for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
|
||||
byte[] invalidSignature = Base58.decode(invalidSignature58);
|
||||
for (ByteArray invalidSignature : invalidBlockSignatures.keySet()) {
|
||||
for (byte[] signature : blockSignatures) {
|
||||
if (Arrays.equals(signature, invalidSignature)) {
|
||||
if (Arrays.equals(signature, invalidSignature.value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1579,12 +1577,23 @@ public class Synchronizer extends Thread {
|
||||
Message getBlockMessage = new GetBlockMessage(signature);
|
||||
|
||||
Message message = peer.getResponse(getBlockMessage);
|
||||
if (message == null || message.getType() != MessageType.BLOCK)
|
||||
if (message == null)
|
||||
return null;
|
||||
|
||||
BlockMessage blockMessage = (BlockMessage) message;
|
||||
switch (message.getType()) {
|
||||
case BLOCK: {
|
||||
BlockMessage blockMessage = (BlockMessage) message;
|
||||
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
|
||||
}
|
||||
|
||||
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
|
||||
case BLOCK_V2: {
|
||||
BlockV2Message blockMessage = (BlockV2Message) message;
|
||||
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStatesHash());
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
|
||||
|
||||
@@ -67,6 +67,9 @@ public class ArbitraryDataFileListManager {
|
||||
/** Maximum number of hops that a file list relay request is allowed to make */
|
||||
public static int RELAY_REQUEST_MAX_HOPS = 4;
|
||||
|
||||
/** Minimum peer version to use relay */
|
||||
public static String RELAY_MIN_PEER_VERSION = "3.4.0";
|
||||
|
||||
|
||||
private ArbitraryDataFileListManager() {
|
||||
}
|
||||
@@ -120,12 +123,22 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Then allow another 5 attempts, each 5 minutes apart
|
||||
// Then allow another 3 attempts, each 5 minutes apart
|
||||
if (timeSinceLastAttempt > 5 * 60 * 1000L) {
|
||||
// We haven't tried for at least 5 minutes
|
||||
|
||||
if (networkBroadcastCount < 5) {
|
||||
// We've made less than 5 total attempts
|
||||
if (networkBroadcastCount < 6) {
|
||||
// We've made less than 6 total attempts
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Then allow another 4 attempts, each 30 minutes apart
|
||||
if (timeSinceLastAttempt > 30 * 60 * 1000L) {
|
||||
// We haven't tried for at least 5 minutes
|
||||
|
||||
if (networkBroadcastCount < 10) {
|
||||
// We've made less than 10 total attempts
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -184,8 +197,8 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (timeSinceLastAttempt > 24 * 60 * 60 * 1000L) {
|
||||
// We haven't tried for at least 24 hours
|
||||
if (timeSinceLastAttempt > 60 * 60 * 1000L) {
|
||||
// We haven't tried for at least 1 hour
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -524,6 +537,7 @@ public class ArbitraryDataFileListManager {
|
||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
|
||||
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
||||
}
|
||||
forwardArbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
// Forward to requesting peer
|
||||
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
|
||||
@@ -690,12 +704,14 @@ public class ArbitraryDataFileListManager {
|
||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||
|
||||
Message relayGetArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops, requestingPeer);
|
||||
relayGetArbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||
Network.getInstance().broadcast(
|
||||
broadcastPeer -> broadcastPeer == peer ||
|
||||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
|
||||
? null : relayGetArbitraryDataFileListMessage);
|
||||
broadcastPeer ->
|
||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
|
||||
);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -22,8 +22,7 @@ import org.qortal.utils.Triple;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.RELAY_REQUEST_MAX_DURATION;
|
||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.RELAY_REQUEST_MAX_HOPS;
|
||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.*;
|
||||
|
||||
public class ArbitraryMetadataManager {
|
||||
|
||||
@@ -339,6 +338,7 @@ public class ArbitraryMetadataManager {
|
||||
if (requestingPeer != null) {
|
||||
|
||||
ArbitraryMetadataMessage forwardArbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, arbitraryMetadataMessage.getArbitraryMetadataFile());
|
||||
forwardArbitraryMetadataMessage.setId(arbitraryMetadataMessage.getId());
|
||||
|
||||
// Forward to requesting peer
|
||||
LOGGER.debug("Forwarding metadata to requesting peer: {}", requestingPeer);
|
||||
@@ -434,12 +434,13 @@ public class ArbitraryMetadataManager {
|
||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||
|
||||
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
|
||||
relayGetArbitraryMetadataMessage.setId(message.getId());
|
||||
|
||||
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||
Network.getInstance().broadcast(
|
||||
broadcastPeer -> broadcastPeer == peer ||
|
||||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
|
||||
? null : relayGetArbitraryMetadataMessage);
|
||||
broadcastPeer ->
|
||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -0,0 +1,913 @@
|
||||
package org.qortal.controller.tradebot;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.rust.litewalletjni.LiteWalletJni;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
/**
|
||||
* Performing cross-chain trading steps on behalf of user.
|
||||
* <p>
|
||||
* We deal with three different independent state-spaces here:
|
||||
* <ul>
|
||||
* <li>Qortal blockchain</li>
|
||||
* <li>Foreign blockchain</li>
|
||||
* <li>Trade-bot entries</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(PirateChainACCTv3TradeBot.class);
|
||||
|
||||
public enum State implements TradeBot.StateNameAndValueSupplier {
|
||||
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
|
||||
BOB_WAITING_FOR_MESSAGE(15, true, true),
|
||||
BOB_WAITING_FOR_AT_REDEEM(25, true, true),
|
||||
BOB_DONE(30, false, false),
|
||||
BOB_REFUNDED(35, false, false),
|
||||
|
||||
ALICE_WAITING_FOR_AT_LOCK(85, true, true),
|
||||
ALICE_DONE(95, false, false),
|
||||
ALICE_REFUNDING_A(105, true, true),
|
||||
ALICE_REFUNDED(110, false, false);
|
||||
|
||||
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
public final int value;
|
||||
public final boolean requiresAtData;
|
||||
public final boolean requiresTradeData;
|
||||
|
||||
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||
this.value = value;
|
||||
this.requiresAtData = requiresAtData;
|
||||
this.requiresTradeData = requiresTradeData;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStateValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||
|
||||
private static PirateChainACCTv3TradeBot instance;
|
||||
|
||||
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
|
||||
.map(State::name)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
|
||||
private PirateChainACCTv3TradeBot() {
|
||||
}
|
||||
|
||||
public static synchronized PirateChainACCTv3TradeBot getInstance() {
|
||||
if (instance == null)
|
||||
instance = new PirateChainACCTv3TradeBot();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getEndStates() {
|
||||
return this.endStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for ARRR.
|
||||
* <p>
|
||||
* Generates:
|
||||
* <ul>
|
||||
* <li>new 'trade' private key</li>
|
||||
* </ul>
|
||||
* Derives:
|
||||
* <ul>
|
||||
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
|
||||
* <li>'foreign' (as in PirateChain) public key, public key hash</li>
|
||||
* </ul>
|
||||
* A Qortal AT is then constructed including the following as constants in the 'data segment':
|
||||
* <ul>
|
||||
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
|
||||
* <li>'foreign'/PirateChain public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||
* <li>QORT amount on offer by Bob</li>
|
||||
* <li>ARRR amount expected in return by Bob (from Alice)</li>
|
||||
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
|
||||
* </ul>
|
||||
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
|
||||
* <p>
|
||||
* Trade-bot will wait for Bob's AT to be deployed before taking next step.
|
||||
* <p>
|
||||
* @param repository
|
||||
* @param tradeBotCreateRequest
|
||||
* @return raw, unsigned DEPLOY_AT transaction
|
||||
* @throws DataException
|
||||
*/
|
||||
public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
|
||||
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||
|
||||
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
|
||||
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
|
||||
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
|
||||
|
||||
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||
|
||||
// ARRR wallet must be loaded before a trade can be created
|
||||
// This is to stop trades from nodes on unsupported architectures (e.g. 32bit)
|
||||
if (!LiteWalletJni.isLoaded()) {
|
||||
throw new DataException("Pirate wallet not found. Check wallets screen for details.");
|
||||
}
|
||||
|
||||
if (!PirateChain.getInstance().isValidAddress(tradeBotCreateRequest.receivingAddress)) {
|
||||
throw new DataException("Unsupported Pirate Chain receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
}
|
||||
|
||||
Bech32.Bech32Data decodedReceivingAddress = Bech32.decode(tradeBotCreateRequest.receivingAddress);
|
||||
byte[] pirateChainReceivingAccountInfo = decodedReceivingAddress.data;
|
||||
|
||||
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
|
||||
|
||||
// Deploy AT
|
||||
long timestamp = NTP.getTime();
|
||||
byte[] reference = creator.getLastReference();
|
||||
long fee = 0L;
|
||||
byte[] signature = null;
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
|
||||
|
||||
String name = "QORT/ARRR ACCT";
|
||||
String description = "QORT/ARRR cross-chain trade";
|
||||
String aTType = "ACCT";
|
||||
String tags = "ACCT QORT ARRR";
|
||||
byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKey, tradeBotCreateRequest.qortAmount,
|
||||
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
|
||||
long amount = tradeBotCreateRequest.fundingQortAmount;
|
||||
|
||||
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
|
||||
String atAddress = deployAtTransactionData.getAtAddress();
|
||||
|
||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, PirateChainACCTv3.NAME,
|
||||
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
|
||||
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
|
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||
null, null,
|
||||
SupportedBlockchain.PIRATECHAIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
tradeBotCreateRequest.foreignAmount, null, null, null, pirateChainReceivingAccountInfo);
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
|
||||
|
||||
// Attempt to backup the trade bot data
|
||||
TradeBot.backupTradeBotData(repository, null);
|
||||
|
||||
// Return to user for signing and broadcast as we don't have their Qortal private key
|
||||
try {
|
||||
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||
} catch (TransformationException e) {
|
||||
throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching ARRR to an existing offer.
|
||||
* <p>
|
||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||
* and access to a PirateChain wallet via <tt>xprv58</tt>.
|
||||
* <p>
|
||||
* The <tt>crossChainTradeData</tt> contains the current trade offer state
|
||||
* as extracted from the AT's data segment.
|
||||
* <p>
|
||||
* Access to a funded wallet is via a PirateChain BIP32 hierarchical deterministic key,
|
||||
* passed via <tt>xprv58</tt>.
|
||||
* <b>This key will be stored in your node's database</b>
|
||||
* to allow trade-bot to create/fund the necessary P2SH transactions!
|
||||
* However, due to the nature of BIP32 keys, it is possible to give the trade-bot
|
||||
* only a subset of wallet access (see BIP32 for more details).
|
||||
* <p>
|
||||
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
|
||||
* Electrum wallet by going to the console tab and entering:<br>
|
||||
* <tt>wallet.keystore.xprv</tt><br>
|
||||
* which should result in a base58 string starting with either 'xprv' (for PirateChain main-net)
|
||||
* or 'tprv' for (PirateChain test-net).
|
||||
* <p>
|
||||
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
|
||||
* <p>
|
||||
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
|
||||
* with the PirateChain amount expected by 'Bob'.
|
||||
* <p>
|
||||
* If the PirateChain transaction is successfully broadcast to the network then
|
||||
* we also send a MESSAGE to Bob's trade-bot to let them know.
|
||||
* <p>
|
||||
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
|
||||
* <p>
|
||||
* @param repository
|
||||
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
|
||||
* @param seed58 funded wallet xprv in base58
|
||||
* @return true if P2SH-A funding transaction successfully broadcast to PirateChain network, false otherwise
|
||||
* @throws DataException
|
||||
*/
|
||||
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String seed58, String receivingAddress) throws DataException {
|
||||
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||
byte[] secretA = TradeBot.generateSecret();
|
||||
byte[] hashOfSecretA = Crypto.hash160(secretA);
|
||||
|
||||
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
|
||||
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
|
||||
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
|
||||
|
||||
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
||||
|
||||
String tradePrivateKey58 = Base58.encode(tradePrivateKey);
|
||||
String tradeForeignPublicKey58 = Base58.encode(tradeForeignPublicKey);
|
||||
String secret58 = Base58.encode(secretA);
|
||||
|
||||
// We need to generate lockTime-A: add tradeTimeout to now
|
||||
long now = NTP.getTime();
|
||||
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
|
||||
|
||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, PirateChainACCTv3.NAME,
|
||||
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
|
||||
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
|
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||
secretA, hashOfSecretA,
|
||||
SupportedBlockchain.PIRATECHAIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
crossChainTradeData.expectedForeignAmount, seed58, null, lockTimeA, receivingPublicKeyHash);
|
||||
|
||||
// Attempt to backup the trade bot data
|
||||
// Include tradeBotData as an additional parameter, since it's not in the repository yet
|
||||
TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData));
|
||||
|
||||
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||
long p2shFee;
|
||||
try {
|
||||
p2shFee = PirateChain.getInstance().getP2shFee(now);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.debug("Couldn't estimate PirateChain fees?");
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
// Do not include fee for funding transaction as this is covered by buildSpend()
|
||||
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
|
||||
|
||||
// P2SH-A to be funded
|
||||
byte[] redeemScriptBytes = PirateChainHTLC.buildScript(tradeForeignPublicKey, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||
String p2shAddressT3 = PirateChain.getInstance().deriveP2shAddress(redeemScriptBytes); // Use t3 prefix when funding
|
||||
byte[] redeemScriptWithPrefixBytes = PirateChainHTLC.buildScriptWithPrefix(tradeForeignPublicKey, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||
String redeemScriptWithPrefix58 = Base58.encode(redeemScriptWithPrefixBytes);
|
||||
|
||||
// Send to P2SH address
|
||||
try {
|
||||
String txid = PirateChain.getInstance().fundP2SH(seed58, p2shAddressT3, amountA, redeemScriptWithPrefix58);
|
||||
LOGGER.info("fundingTxidHex: {}", txid);
|
||||
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.debug("Unable to build and send P2SH-A funding transaction - lack of funds?");
|
||||
return ResponseResult.BALANCE_ISSUE;
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = PirateChainACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKey(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddressT3));
|
||||
|
||||
return ResponseResult.OK;
|
||||
}
|
||||
|
||||
public static String hex(byte[] bytes) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (byte aByte : bytes) {
|
||||
result.append(String.format("%02x", aByte));
|
||||
// upper case
|
||||
// result.append(String.format("%02X", aByte));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||
if (tradeBotState == null)
|
||||
return true;
|
||||
|
||||
// If the AT doesn't exist then we might as well let the user tidy up
|
||||
if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
|
||||
return true;
|
||||
|
||||
switch (tradeBotState) {
|
||||
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||
case ALICE_DONE:
|
||||
case BOB_DONE:
|
||||
case ALICE_REFUNDED:
|
||||
case BOB_REFUNDED:
|
||||
case ALICE_REFUNDING_A:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
|
||||
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||
if (tradeBotState == null) {
|
||||
LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
ATData atData = null;
|
||||
CrossChainTradeData tradeData = null;
|
||||
|
||||
if (tradeBotState.requiresAtData) {
|
||||
// Attempt to fetch AT data
|
||||
atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
|
||||
if (atData == null) {
|
||||
LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tradeBotState.requiresTradeData) {
|
||||
tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||
if (tradeData == null) {
|
||||
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (tradeBotState) {
|
||||
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||
handleBobWaitingForAtConfirm(repository, tradeBotData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_MESSAGE:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_WAITING_FOR_AT_LOCK:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_AT_REDEEM:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_DONE:
|
||||
case BOB_DONE:
|
||||
break;
|
||||
|
||||
case ALICE_REFUNDING_A:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_REFUNDED:
|
||||
case BOB_REFUNDED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is waiting for Bob's AT to deploy.
|
||||
* <p>
|
||||
* If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
|
||||
*/
|
||||
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
|
||||
if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
|
||||
return;
|
||||
|
||||
// We've waited ages for AT to be confirmed into a block but something has gone awry.
|
||||
// After this long we assume transaction loss so give up with trade-bot entry too.
|
||||
tradeBotData.setState(State.BOB_REFUNDED.name());
|
||||
tradeBotData.setStateValue(State.BOB_REFUNDED.value);
|
||||
tradeBotData.setTimestamp(NTP.getTime());
|
||||
// We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
|
||||
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
|
||||
TradeBot.notifyStateChange(tradeBotData);
|
||||
return;
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
|
||||
() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
|
||||
* <p>
|
||||
* It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
|
||||
* in which case trade-bot is done with this specific trade and finalizes on refunded state.
|
||||
* <p>
|
||||
* Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
|
||||
* <p>
|
||||
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
|
||||
* <p>
|
||||
* Assuming P2SH-A has at least expected PirateChain balance,
|
||||
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
|
||||
* <p>
|
||||
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
|
||||
* <p>
|
||||
* Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
|
||||
* extract secret-A needed to redeem Alice's P2SH.
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
// If AT has finished then Bob likely cancelled his trade offer
|
||||
if (atData.getIsFinished()) {
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||
() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
String address = tradeBotData.getTradeNativeAddress();
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
|
||||
|
||||
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
|
||||
if (messageTransactionData.isText())
|
||||
continue;
|
||||
|
||||
// We're expecting: HASH160(secret-A), Alice's PirateChain pubkeyhash and lockTime-A
|
||||
byte[] messageData = messageTransactionData.getData();
|
||||
PirateChainACCTv3.OfferMessageData offerMessageData = PirateChainACCTv3.extractOfferMessageData(messageData);
|
||||
if (offerMessageData == null)
|
||||
continue;
|
||||
|
||||
byte[] aliceForeignPublicKey = offerMessageData.partnerPirateChainPublicKey;
|
||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||
|
||||
// Determine P2SH-A address and confirm funded
|
||||
byte[] redeemScriptA = PirateChainHTLC.buildScript(aliceForeignPublicKey, lockTimeA, tradeBotData.getTradeForeignPublicKey(), hashOfSecretA);
|
||||
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||
|
||||
PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// There might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||
continue;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
// We've already redeemed this?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddress));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
// This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||
continue;
|
||||
|
||||
case FUNDED:
|
||||
// Fall-through out of switch...
|
||||
break;
|
||||
}
|
||||
|
||||
// Good to go - send MESSAGE to AT
|
||||
|
||||
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
|
||||
|
||||
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
||||
byte[] outgoingMessageData = PirateChainACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
String messageRecipient = tradeBotData.getAtAddress();
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
|
||||
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
|
||||
* <p>
|
||||
* It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
|
||||
* this process has taken so long that we've reached P2SH-A's locktime, or that someone else
|
||||
* has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
|
||||
* <p>
|
||||
* Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
|
||||
* <p>
|
||||
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
|
||||
* <p>
|
||||
* In revealing a valid secret-A, Bob can then redeem the ARRR funds from P2SH-A.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
|
||||
return;
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
|
||||
// Refund P2SH-A if we've passed lockTime-A
|
||||
if (NTP.getTime() >= lockTimeA * 1000L) {
|
||||
byte[] redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
case FUNDED:
|
||||
break;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
// Already redeemed?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddress));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddress));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> atData.getIsFinished()
|
||||
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddress)
|
||||
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddress));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We're waiting for AT to be in TRADE mode
|
||||
if (crossChainTradeData.mode != AcctMode.TRADING)
|
||||
return;
|
||||
|
||||
// AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
|
||||
|
||||
// Find our MESSAGE to AT from previous state
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
|
||||
crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
|
||||
if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
|
||||
LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
||||
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
|
||||
|
||||
// Our calculated refundTimeout should match AT's refundTimeout
|
||||
if (refundTimeout != crossChainTradeData.refundTimeout) {
|
||||
LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
|
||||
// We'll eventually refund
|
||||
return;
|
||||
}
|
||||
|
||||
// We're good to redeem AT
|
||||
|
||||
// Send 'redeem' MESSAGE to AT using both secret
|
||||
byte[] secretA = tradeBotData.getSecret();
|
||||
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
|
||||
byte[] messageData = PirateChainACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
|
||||
String messageRecipient = tradeBotData.getAtAddress();
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||
() -> String.format("Redeeming AT %s. Funds should arrive at %s",
|
||||
tradeBotData.getAtAddress(), qortalReceivingAddress));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the ARRR funds from P2SH-A.
|
||||
* <p>
|
||||
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
|
||||
* trade-bot is done with this specific trade and finalizes in refunded state.
|
||||
* <p>
|
||||
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the ARRR funds from P2SH-A
|
||||
* to Bob's 'foreign'/PirateChain trade legacy-format address, as derived from trade private key.
|
||||
* <p>
|
||||
* (This could potentially be 'improved' to send ARRR to any address of Bob's choosing by changing the transaction output).
|
||||
* <p>
|
||||
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
// AT should be 'finished' once Alice has redeemed QORT funds
|
||||
if (!atData.getIsFinished())
|
||||
// Not finished yet
|
||||
return;
|
||||
|
||||
// If AT is REFUNDED or CANCELLED then something has gone wrong
|
||||
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
|
||||
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the ARRR
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] secretA = PirateChainACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
|
||||
if (secretA == null) {
|
||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use secret-A to redeem P2SH-A
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
byte[] redeemScriptA = PirateChainHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
|
||||
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); // Use 't3' prefix when refunding
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
String receivingAddress = Bech32.encode("zs", receivingAccountInfo);
|
||||
|
||||
PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
|
||||
return;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
// Double-check that we have redeemed P2SH-A...
|
||||
break;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
// Wait for AT to auto-refund
|
||||
return;
|
||||
|
||||
case FUNDED: {
|
||||
// Get funding txid
|
||||
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
if (fundingTxidHex == null) {
|
||||
throw new ForeignBlockchainException("Missing funding txid when redeeming P2SH");
|
||||
}
|
||||
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
|
||||
|
||||
// Redeem P2SH
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
byte[] privateKey = tradeBotData.getTradePrivateKey();
|
||||
String secret58 = Base58.encode(secretA);
|
||||
String privateKey58 = Base58.encode(privateKey);
|
||||
String redeemScript58 = Base58.encode(redeemScriptA);
|
||||
|
||||
String txid = PirateChain.getInstance().redeemP2sh(p2shAddressT3, receivingAddress, redeemAmount.value,
|
||||
redeemScript58, fundingTxid58, secret58, privateKey58);
|
||||
LOGGER.info("Redeem txid: {}", txid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is attempting to refund P2SH-A.
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
|
||||
// We can't refund P2SH-A until lockTime-A has passed
|
||||
if (NTP.getTime() <= lockTimeA * 1000L)
|
||||
return;
|
||||
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = pirateChain.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTimeA)
|
||||
return;
|
||||
|
||||
byte[] redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
|
||||
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); // Use 't3' prefix when refunding
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// Still waiting for P2SH-A to be funded...
|
||||
return;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
// Too late!
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||
() -> String.format("P2SH-A %s already spent!", p2shAddress));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
break;
|
||||
|
||||
case FUNDED:{
|
||||
// Get funding txid
|
||||
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
if (fundingTxidHex == null) {
|
||||
throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
|
||||
}
|
||||
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
|
||||
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
byte[] privateKey = tradeBotData.getTradePrivateKey();
|
||||
String privateKey58 = Base58.encode(privateKey);
|
||||
String redeemScript58 = Base58.encode(redeemScriptA);
|
||||
String receivingAddress = pirateChain.getWalletAddress(tradeBotData.getForeignKey());
|
||||
|
||||
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
|
||||
receivingAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTimeA, privateKey58);
|
||||
LOGGER.info("Refund txid: {}", txid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddress));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
|
||||
* <p>
|
||||
* Will automatically update trade-bot state to <tt>ALICE_REFUNDING_A</tt> or <tt>ALICE_DONE</tt> as necessary.
|
||||
*
|
||||
* @throws DataException
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
// This is OK
|
||||
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
|
||||
return false;
|
||||
|
||||
boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
|
||||
|
||||
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
|
||||
if (isAtLockedToUs) {
|
||||
// AT is trading with us - OK
|
||||
return false;
|
||||
} else {
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
|
||||
// We've redeemed already?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||
() -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
|
||||
} else {
|
||||
// Any other state is not good, so start defensive refund
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
|
||||
return (lockTimeA - tradeTimeout * 60) * 1000L;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -103,6 +103,7 @@ public class TradeBot implements Listener {
|
||||
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(PirateChainACCTv3.class, PirateChainACCTv3TradeBot::getInstance);
|
||||
}
|
||||
|
||||
private static TradeBot instance;
|
||||
@@ -242,8 +243,8 @@ public class TradeBot implements Listener {
|
||||
if (!(event instanceof Synchronizer.NewChainTipEvent))
|
||||
return;
|
||||
|
||||
// Don't process trade bots or broadcast presence timestamps if our chain is more than 30 minutes old
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
|
||||
// Don't process trade bots or broadcast presence timestamps if our chain is more than 60 minutes old
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
|
||||
return;
|
||||
|
||||
@@ -292,14 +293,14 @@ public class TradeBot implements Listener {
|
||||
}
|
||||
|
||||
public static byte[] deriveTradeNativePublicKey(byte[] privateKey) {
|
||||
return PrivateKeyAccount.toPublicKey(privateKey);
|
||||
return Crypto.toPublicKey(privateKey);
|
||||
}
|
||||
|
||||
public static byte[] deriveTradeForeignPublicKey(byte[] privateKey) {
|
||||
return ECKey.fromPrivate(privateKey).getPubKey();
|
||||
}
|
||||
|
||||
/*package*/ static byte[] generateSecret() {
|
||||
/*package*/ public static byte[] generateSecret() {
|
||||
byte[] secret = new byte[32];
|
||||
RANDOM.nextBytes(secret);
|
||||
return secret;
|
||||
|
||||
@@ -174,6 +174,8 @@ public class Bitcoin extends Bitcoiny {
|
||||
Context bitcoinjContext = new Context(bitcoinNet.getParams());
|
||||
|
||||
instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
electrumX.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.bitcoinj.wallet.SendRequest;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.qortal.api.model.SimpleForeignTransaction;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
@@ -42,7 +43,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
public static final int HASH160_LENGTH = 20;
|
||||
|
||||
protected final BitcoinyBlockchainProvider blockchain;
|
||||
protected final BitcoinyBlockchainProvider blockchainProvider;
|
||||
protected final Context bitcoinjContext;
|
||||
protected final String currencyCode;
|
||||
|
||||
@@ -61,18 +62,13 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
/** How many wallet keys to generate in each batch. */
|
||||
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3;
|
||||
|
||||
/** How many wallet keys to generate when using bitcoinj as the data provider.
|
||||
* We must use a higher value here since we are unable to request multiple batches of keys.
|
||||
* Without this, the bitcoinj state can be missing transactions, causing errors such as "insufficient balance". */
|
||||
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ = 50;
|
||||
|
||||
/** Byte offset into raw block headers to block timestamp. */
|
||||
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||
|
||||
// Constructors and instance
|
||||
|
||||
protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
this.blockchain = blockchain;
|
||||
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) {
|
||||
this.blockchainProvider = blockchainProvider;
|
||||
this.bitcoinjContext = bitcoinjContext;
|
||||
this.currencyCode = currencyCode;
|
||||
|
||||
@@ -82,7 +78,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
// Getters & setters
|
||||
|
||||
public BitcoinyBlockchainProvider getBlockchainProvider() {
|
||||
return this.blockchain;
|
||||
return this.blockchainProvider;
|
||||
}
|
||||
|
||||
public Context getBitcoinjContext() {
|
||||
@@ -155,10 +151,10 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
public int getMedianBlockTime() throws ForeignBlockchainException {
|
||||
int height = this.blockchain.getCurrentHeight();
|
||||
int height = this.blockchainProvider.getCurrentHeight();
|
||||
|
||||
// Grab latest 11 blocks
|
||||
List<byte[]> blockHeaders = this.blockchain.getRawBlockHeaders(height - 11, 11);
|
||||
List<byte[]> blockHeaders = this.blockchainProvider.getRawBlockHeaders(height - 11, 11);
|
||||
if (blockHeaders.size() < 11)
|
||||
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
|
||||
|
||||
@@ -197,7 +193,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* @throws ForeignBlockchainException if there was an error
|
||||
*/
|
||||
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
|
||||
return this.blockchain.getConfirmedBalance(addressToScriptPubKey(base58Address));
|
||||
return this.blockchainProvider.getConfirmedBalance(addressToScriptPubKey(base58Address));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +204,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
*/
|
||||
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
|
||||
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
|
||||
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
|
||||
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
|
||||
|
||||
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
@@ -228,7 +224,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
*/
|
||||
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
|
||||
public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
|
||||
byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash);
|
||||
byte[] rawTransactionBytes = this.blockchainProvider.getRawTransaction(txHash);
|
||||
|
||||
Context.propagate(bitcoinjContext);
|
||||
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
|
||||
@@ -245,7 +241,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
ForeignBlockchainException e2 = null;
|
||||
while (retries <= 3) {
|
||||
try {
|
||||
return this.blockchain.getAddressTransactions(scriptPubKey, includeUnconfirmed);
|
||||
return this.blockchainProvider.getAddressTransactions(scriptPubKey, includeUnconfirmed);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
e2 = e;
|
||||
retries++;
|
||||
@@ -261,7 +257,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* @throws ForeignBlockchainException if there was an error.
|
||||
*/
|
||||
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
return this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
|
||||
return this.blockchainProvider.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,11 +266,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* @throws ForeignBlockchainException if there was an error
|
||||
*/
|
||||
public List<byte[]> getAddressTransactions(String base58Address) throws ForeignBlockchainException {
|
||||
List<TransactionHash> transactionHashes = this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), false);
|
||||
List<TransactionHash> transactionHashes = this.blockchainProvider.getAddressTransactions(addressToScriptPubKey(base58Address), false);
|
||||
|
||||
List<byte[]> rawTransactions = new ArrayList<>();
|
||||
for (TransactionHash transactionInfo : transactionHashes) {
|
||||
byte[] rawTransaction = this.blockchain.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
|
||||
byte[] rawTransaction = this.blockchainProvider.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
|
||||
rawTransactions.add(rawTransaction);
|
||||
}
|
||||
|
||||
@@ -292,7 +288,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
ForeignBlockchainException e2 = null;
|
||||
while (retries <= 3) {
|
||||
try {
|
||||
return this.blockchain.getTransaction(txHash);
|
||||
return this.blockchainProvider.getTransaction(txHash);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
e2 = e;
|
||||
retries++;
|
||||
@@ -307,7 +303,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
|
||||
this.blockchain.broadcastTransaction(transaction.bitcoinSerialize());
|
||||
this.blockchainProvider.broadcastTransaction(transaction.bitcoinSerialize());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -360,22 +356,25 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* @param key58 BIP32/HD extended Bitcoin private/public key
|
||||
* @return unspent BTC balance, or null if unable to determine balance
|
||||
*/
|
||||
public Long getWalletBalance(String key58) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
|
||||
// It's more accurate to calculate the balance from the transactions, rather than asking Bitcoinj
|
||||
return this.getWalletBalanceFromTransactions(key58);
|
||||
|
||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
||||
|
||||
Coin balance = wallet.getBalance();
|
||||
if (balance == null)
|
||||
return null;
|
||||
|
||||
return balance.value;
|
||||
// Context.propagate(bitcoinjContext);
|
||||
//
|
||||
// Wallet wallet = walletFromDeterministicKey58(key58);
|
||||
// wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
||||
//
|
||||
// Coin balance = wallet.getBalance();
|
||||
// if (balance == null)
|
||||
// return null;
|
||||
//
|
||||
// return balance.value;
|
||||
}
|
||||
|
||||
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
||||
long balance = 0;
|
||||
Comparator<SimpleTransaction> oldestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp);
|
||||
Comparator<SimpleTransaction> oldestTimestampFirstComparator = Comparator.comparingLong(SimpleTransaction::getTimestamp);
|
||||
List<SimpleTransaction> transactions = getWalletTransactions(key58).stream().sorted(oldestTimestampFirstComparator).collect(Collectors.toList());
|
||||
for (SimpleTransaction transaction : transactions) {
|
||||
balance += transaction.getTotalAmount();
|
||||
@@ -409,9 +408,6 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
||||
Set<String> keySet = new HashSet<>();
|
||||
|
||||
// Set the number of consecutive empty batches required before giving up
|
||||
final int numberOfAdditionalBatchesToSearch = 7;
|
||||
|
||||
int unusedCounter = 0;
|
||||
int ki = 0;
|
||||
do {
|
||||
@@ -438,12 +434,12 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
if (areAllKeysUnused) {
|
||||
// No transactions
|
||||
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
|
||||
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
|
||||
// ... and we've hit our search limit
|
||||
break;
|
||||
}
|
||||
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||
unusedCounter++;
|
||||
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
|
||||
} else {
|
||||
// Some keys in this batch were used, so reset the counter
|
||||
unusedCounter = 0;
|
||||
@@ -455,7 +451,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
// Process new keys
|
||||
} while (true);
|
||||
|
||||
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
|
||||
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingLong(SimpleTransaction::getTimestamp).reversed();
|
||||
|
||||
// Update cache and return
|
||||
transactionsCacheTimestamp = NTP.getTime();
|
||||
@@ -537,7 +533,8 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
// All inputs and outputs relate to this wallet, so the balance should be unaffected
|
||||
amount = 0;
|
||||
}
|
||||
return new SimpleTransaction(t.txHash, t.timestamp, amount, fee, inputs, outputs);
|
||||
long timestampMillis = t.timestamp * 1000L;
|
||||
return new SimpleTransaction(t.txHash, timestampMillis, amount, fee, inputs, outputs, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -573,7 +570,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(script, false);
|
||||
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(script, false);
|
||||
|
||||
/*
|
||||
* If there are no unspent outputs then either:
|
||||
@@ -591,7 +588,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
}
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
|
||||
List<TransactionHash> historicTransactionHashes = this.blockchainProvider.getAddressTransactions(script, false);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
// Fully spent key - case (a)
|
||||
@@ -629,7 +626,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
this.keyChain = this.wallet.getActiveKeyChain();
|
||||
|
||||
// Set up wallet's key chain
|
||||
this.keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ);
|
||||
this.keyChain.setLookaheadSize(Settings.getInstance().getBitcoinjLookaheadSize());
|
||||
this.keyChain.maybeLookAhead();
|
||||
}
|
||||
|
||||
@@ -650,7 +647,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
List<UnspentOutput> unspentOutputs;
|
||||
try {
|
||||
unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false);
|
||||
unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
|
||||
}
|
||||
@@ -674,7 +671,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<TransactionHash> historicTransactionHashes;
|
||||
try {
|
||||
historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false);
|
||||
historicTransactionHashes = this.bitcoiny.blockchainProvider.getAddressTransactions(script, false);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
|
||||
}
|
||||
@@ -727,7 +724,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
@Override
|
||||
public int getChainHeadHeight() throws UTXOProviderException {
|
||||
try {
|
||||
return this.bitcoiny.blockchain.getCurrentHeight();
|
||||
return this.bitcoiny.blockchainProvider.getCurrentHeight();
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class BitcoinyBlockchainProvider {
|
||||
@@ -7,18 +9,32 @@ public abstract class BitcoinyBlockchainProvider {
|
||||
public static final boolean INCLUDE_UNCONFIRMED = true;
|
||||
public static final boolean EXCLUDE_UNCONFIRMED = false;
|
||||
|
||||
/** Sets the blockchain using this provider instance */
|
||||
public abstract void setBlockchain(Bitcoiny blockchain);
|
||||
|
||||
/** Returns ID unique to bitcoiny network (e.g. "Litecoin-TEST3") */
|
||||
public abstract String getNetId();
|
||||
|
||||
/** Returns current blockchain height. */
|
||||
public abstract int getCurrentHeight() throws ForeignBlockchainException;
|
||||
|
||||
/** Returns a list of compact blocks, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max.
|
||||
* Used for Pirate/Zcash only. If ever needed for other blockchains, the response format will need to be
|
||||
* made generic. */
|
||||
public abstract List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns a list of raw block headers, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
|
||||
public abstract List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns a list of block timestamps, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
|
||||
public abstract List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
|
||||
public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns balance of base58 encoded address. */
|
||||
public abstract long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
|
||||
public abstract byte[] getRawTransaction(String txHash) throws ForeignBlockchainException;
|
||||
|
||||
@@ -31,6 +47,12 @@ public abstract class BitcoinyBlockchainProvider {
|
||||
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
|
||||
public abstract List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns list of BitcoinyTransaction objects for <tt>address</tt>, optionally including unconfirmed transactions. */
|
||||
public abstract List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns list of unspent transaction outputs for <tt>address</tt>, optionally including unconfirmed transactions. */
|
||||
public abstract List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||
|
||||
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
|
||||
public abstract List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -10,8 +11,13 @@ import javax.xml.bind.annotation.XmlTransient;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class BitcoinyTransaction {
|
||||
|
||||
public static final Comparator<BitcoinyTransaction> CONFIRMED_FIRST = (a, b) -> Boolean.compare(a.height != 0, b.height != 0);
|
||||
|
||||
public final String txHash;
|
||||
|
||||
@XmlTransient
|
||||
public Integer height;
|
||||
|
||||
@XmlTransient
|
||||
public final int size;
|
||||
|
||||
@@ -113,6 +119,10 @@ public class BitcoinyTransaction {
|
||||
this.totalAmount = outputs.stream().map(output -> output.value).reduce(0L, Long::sum);
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("txHash %s, size %d, locktime %d, timestamp %d\n"
|
||||
+ "\tinputs: [%s]\n"
|
||||
|
||||
@@ -135,6 +135,8 @@ public class Dogecoin extends Bitcoiny {
|
||||
Context bitcoinjContext = new Context(dogecoinNet.getParams());
|
||||
|
||||
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
electrumX.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.simple.JSONArray;
|
||||
@@ -107,6 +108,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
private final String netId;
|
||||
private final String expectedGenesisHash;
|
||||
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
|
||||
private Bitcoiny blockchain;
|
||||
|
||||
private final Object serverLock = new Object();
|
||||
private Server currentServer;
|
||||
@@ -135,6 +137,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
|
||||
// Methods for use by other classes
|
||||
|
||||
@Override
|
||||
public void setBlockchain(Bitcoiny blockchain) {
|
||||
this.blockchain = blockchain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNetId() {
|
||||
return this.netId;
|
||||
@@ -161,6 +168,16 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
return ((Long) heightObj).intValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of raw blocks, starting from <tt>startHeight</tt> inclusive.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
|
||||
throw new ForeignBlockchainException("getCompactBlocks not implemented for ElectrumX due to being specific to zcash");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
|
||||
* <p>
|
||||
@@ -222,6 +239,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
return rawBlockHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of raw block timestamps, starting from <tt>startHeight</tt> inclusive.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException {
|
||||
// FUTURE: implement this if needed. For now we use getRawBlockHeaders directly
|
||||
throw new ForeignBlockchainException("getBlockTimestamps not yet implemented for ElectrumX");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns confirmed balance, based on passed payment script.
|
||||
* <p>
|
||||
@@ -247,6 +275,29 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
return (Long) balanceJson.get("confirmed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns confirmed balance, based on passed base58 encoded address.
|
||||
* <p>
|
||||
* @return confirmed balance, or zero if address unknown
|
||||
* @throws ForeignBlockchainException if there was an error
|
||||
*/
|
||||
@Override
|
||||
public long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException {
|
||||
throw new ForeignBlockchainException("getConfirmedAddressBalance not yet implemented for ElectrumX");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of unspent outputs pertaining to passed address.
|
||||
* <p>
|
||||
* @return list of unspent outputs, or empty list if address unknown
|
||||
* @throws ForeignBlockchainException if there was an error.
|
||||
*/
|
||||
@Override
|
||||
public List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
byte[] script = this.blockchain.addressToScriptPubKey(address);
|
||||
return this.getUnspentOutputs(script, includeUnconfirmed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of unspent outputs pertaining to passed payment script.
|
||||
* <p>
|
||||
@@ -482,6 +533,12 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
return transactionHashes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
// FUTURE: implement this if needed. For now we use getAddressTransactions() + getTransaction()
|
||||
throw new ForeignBlockchainException("getAddressBitcoinyTransactions not yet implemented for ElectrumX");
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts raw transaction to network.
|
||||
* <p>
|
||||
@@ -682,6 +739,10 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
} catch (IOException | NoSuchElementException e) {
|
||||
// Unable to send, or receive -- try another server?
|
||||
return null;
|
||||
} catch (NoSuchMethodError e) {
|
||||
// Likely an SSL dependency issue - retries are unlikely to succeed
|
||||
LOGGER.error("ElectrumX output stream error", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
254
src/main/java/org/qortal/crosschain/LegacyZcashAddress.java
Normal file
254
src/main/java/org/qortal/crosschain/LegacyZcashAddress.java
Normal file
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
* Copyright 2014 Giannis Dzegoutanis
|
||||
* Copyright 2015 Andreas Schildbach
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* Updated for Zcash in May 2022 by Qortal core dev team. Modifications allow
|
||||
* correct encoding of P2SH (t3) addresses only. */
|
||||
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.params.Networks;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* <p>A Bitcoin address looks like 1MsScoe2fTJoq4ZPdQgqyhgWeoNamYPevy and is derived from an elliptic curve public key
|
||||
* plus a set of network parameters. Not to be confused with a {@link PeerAddress} or {@link AddressMessage}
|
||||
* which are about network (TCP) addresses.</p>
|
||||
*
|
||||
* <p>A standard address is built by taking the RIPE-MD160 hash of the public key bytes, with a version prefix and a
|
||||
* checksum suffix, then encoding it textually as base58. The version prefix is used to both denote the network for
|
||||
* which the address is valid (see {@link NetworkParameters}, and also to indicate how the bytes inside the address
|
||||
* should be interpreted. Whilst almost all addresses today are hashes of public keys, another (currently unsupported
|
||||
* type) can contain a hash of a script instead.</p>
|
||||
*/
|
||||
public class LegacyZcashAddress extends Address {
|
||||
/**
|
||||
* An address is a RIPEMD160 hash of a public key, therefore is always 160 bits or 20 bytes.
|
||||
*/
|
||||
public static final int LENGTH = 20;
|
||||
|
||||
/** True if P2SH, false if P2PKH. */
|
||||
public final boolean p2sh;
|
||||
|
||||
/* Zcash P2SH header bytes */
|
||||
private static int P2SH_HEADER_1 = 28;
|
||||
private static int P2SH_HEADER_2 = 189;
|
||||
|
||||
/**
|
||||
* Private constructor. Use {@link #fromBase58(NetworkParameters, String)},
|
||||
* {@link #fromPubKeyHash(NetworkParameters, byte[])}, {@link #fromScriptHash(NetworkParameters, byte[])} or
|
||||
* {@link #fromKey(NetworkParameters, ECKey)}.
|
||||
*
|
||||
* @param params
|
||||
* network this address is valid for
|
||||
* @param p2sh
|
||||
* true if hash160 is hash of a script, false if it is hash of a pubkey
|
||||
* @param hash160
|
||||
* 20-byte hash of pubkey or script
|
||||
*/
|
||||
private LegacyZcashAddress(NetworkParameters params, boolean p2sh, byte[] hash160) throws AddressFormatException {
|
||||
super(params, hash160);
|
||||
if (hash160.length != 20)
|
||||
throw new AddressFormatException.InvalidDataLength(
|
||||
"Legacy addresses are 20 byte (160 bit) hashes, but got: " + hash160.length);
|
||||
this.p2sh = p2sh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link LegacyZcashAddress} that represents the given pubkey hash. The resulting address will be a P2PKH type of
|
||||
* address.
|
||||
*
|
||||
* @param params
|
||||
* network this address is valid for
|
||||
* @param hash160
|
||||
* 20-byte pubkey hash
|
||||
* @return constructed address
|
||||
*/
|
||||
public static LegacyZcashAddress fromPubKeyHash(NetworkParameters params, byte[] hash160) throws AddressFormatException {
|
||||
return new LegacyZcashAddress(params, false, hash160);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link LegacyZcashAddress} that represents the public part of the given {@link ECKey}. Note that an address is
|
||||
* derived from a hash of the public key and is not the public key itself.
|
||||
*
|
||||
* @param params
|
||||
* network this address is valid for
|
||||
* @param key
|
||||
* only the public part is used
|
||||
* @return constructed address
|
||||
*/
|
||||
public static LegacyZcashAddress fromKey(NetworkParameters params, ECKey key) {
|
||||
return fromPubKeyHash(params, key.getPubKeyHash());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link LegacyZcashAddress} that represents the given P2SH script hash.
|
||||
*
|
||||
* @param params
|
||||
* network this address is valid for
|
||||
* @param hash160
|
||||
* P2SH script hash
|
||||
* @return constructed address
|
||||
*/
|
||||
public static LegacyZcashAddress fromScriptHash(NetworkParameters params, byte[] hash160) throws AddressFormatException {
|
||||
return new LegacyZcashAddress(params, true, hash160);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link LegacyZcashAddress} from its base58 form.
|
||||
*
|
||||
* @param params
|
||||
* expected network this address is valid for, or null if if the network should be derived from the
|
||||
* base58
|
||||
* @param base58
|
||||
* base58-encoded textual form of the address
|
||||
* @throws AddressFormatException
|
||||
* if the given base58 doesn't parse or the checksum is invalid
|
||||
* @throws AddressFormatException.WrongNetwork
|
||||
* if the given address is valid but for a different chain (eg testnet vs mainnet)
|
||||
*/
|
||||
public static LegacyZcashAddress fromBase58(@Nullable NetworkParameters params, String base58)
|
||||
throws AddressFormatException, AddressFormatException.WrongNetwork {
|
||||
byte[] versionAndDataBytes = Base58.decodeChecked(base58);
|
||||
int version = versionAndDataBytes[0] & 0xFF;
|
||||
byte[] bytes = Arrays.copyOfRange(versionAndDataBytes, 1, versionAndDataBytes.length);
|
||||
if (params == null) {
|
||||
for (NetworkParameters p : Networks.get()) {
|
||||
if (version == p.getAddressHeader())
|
||||
return new LegacyZcashAddress(p, false, bytes);
|
||||
else if (version == p.getP2SHHeader())
|
||||
return new LegacyZcashAddress(p, true, bytes);
|
||||
}
|
||||
throw new AddressFormatException.InvalidPrefix("No network found for " + base58);
|
||||
} else {
|
||||
if (version == params.getAddressHeader())
|
||||
return new LegacyZcashAddress(params, false, bytes);
|
||||
else if (version == params.getP2SHHeader())
|
||||
return new LegacyZcashAddress(params, true, bytes);
|
||||
throw new AddressFormatException.WrongNetwork(version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version header of an address. This is the first byte of a base58 encoded address.
|
||||
*
|
||||
* @return version header as one byte
|
||||
*/
|
||||
public int getVersion() {
|
||||
return p2sh ? params.getP2SHHeader() : params.getAddressHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base58-encoded textual form, including version and checksum bytes.
|
||||
*
|
||||
* @return textual form
|
||||
*/
|
||||
public String toBase58() {
|
||||
return this.encodeChecked(getVersion(), bytes);
|
||||
}
|
||||
|
||||
/** The (big endian) 20 byte hash that is the core of a Bitcoin address. */
|
||||
@Override
|
||||
public byte[] getHash() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of output script that will be used for sending to the address. This is either
|
||||
* {@link ScriptType#P2PKH} or {@link ScriptType#P2SH}.
|
||||
*
|
||||
* @return type of output script
|
||||
*/
|
||||
@Override
|
||||
public ScriptType getOutputScriptType() {
|
||||
return p2sh ? ScriptType.P2SH : ScriptType.P2PKH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an address, examines the version byte and attempts to find a matching NetworkParameters. If you aren't sure
|
||||
* which network the address is intended for (eg, it was provided by a user), you can use this to decide if it is
|
||||
* compatible with the current wallet.
|
||||
*
|
||||
* @return network the address is valid for
|
||||
* @throws AddressFormatException if the given base58 doesn't parse or the checksum is invalid
|
||||
*/
|
||||
public static NetworkParameters getParametersFromAddress(String address) throws AddressFormatException {
|
||||
return LegacyZcashAddress.fromBase58(null, address).getParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
LegacyZcashAddress other = (LegacyZcashAddress) o;
|
||||
return super.equals(other) && this.p2sh == other.p2sh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), p2sh);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toBase58();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LegacyZcashAddress clone() throws CloneNotSupportedException {
|
||||
return (LegacyZcashAddress) super.clone();
|
||||
}
|
||||
|
||||
public static String encodeChecked(int version, byte[] payload) {
|
||||
if (version < 0 || version > 255)
|
||||
throw new IllegalArgumentException("Version not in range.");
|
||||
|
||||
// A stringified buffer is:
|
||||
// 1 byte version + data bytes + 4 bytes check code (a truncated hash)
|
||||
byte[] addressBytes = new byte[2 + payload.length + 4];
|
||||
addressBytes[0] = (byte) P2SH_HEADER_1;
|
||||
addressBytes[1] = (byte) P2SH_HEADER_2;
|
||||
System.arraycopy(payload, 0, addressBytes, 2, payload.length);
|
||||
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length + 2);
|
||||
System.arraycopy(checksum, 0, addressBytes, payload.length + 2, 4);
|
||||
return Base58.encode(addressBytes);
|
||||
}
|
||||
|
||||
// // Comparator for LegacyAddress, left argument must be LegacyAddress, right argument can be any Address
|
||||
// private static final Comparator<Address> LEGACY_ADDRESS_COMPARATOR = Address.PARTIAL_ADDRESS_COMPARATOR
|
||||
// .thenComparingInt(a -> ((LegacyZcashAddress) a).getVersion()) // Then compare Legacy address version byte
|
||||
// .thenComparing(a -> a.bytes, UnsignedBytes.lexicographicalComparator()); // Then compare Legacy bytes
|
||||
//
|
||||
// /**
|
||||
// * {@inheritDoc}
|
||||
// *
|
||||
// * @param o other {@code Address} object
|
||||
// * @return comparison result
|
||||
// */
|
||||
// @Override
|
||||
// public int compareTo(Address o) {
|
||||
// return LEGACY_ADDRESS_COMPARATOR.compare(this, o);
|
||||
// }
|
||||
}
|
||||
@@ -145,6 +145,8 @@ public class Litecoin extends Bitcoiny {
|
||||
Context bitcoinjContext = new Context(litecoinNet.getParams());
|
||||
|
||||
instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
electrumX.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
||||
647
src/main/java/org/qortal/crosschain/PirateChain.java
Normal file
647
src/main/java/org/qortal/crosschain/PirateChain.java
Normal file
@@ -0,0 +1,647 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.rust.litewalletjni.LiteWalletJni;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.libdohj.params.LitecoinRegTestParams;
|
||||
import org.libdohj.params.LitecoinTestNet3Params;
|
||||
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.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
|
||||
public class PirateChain extends Bitcoiny {
|
||||
|
||||
public static final String CURRENCY_CODE = "ARRR";
|
||||
|
||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(10000); // 0.0001 ARRR per 1000 bytes
|
||||
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 10000; // 0.0001 ARRR minimum order, to avoid dust errors // TODO: increase this
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long MAINNET_FEE = 10000L; // 0.0001 ARRR
|
||||
private static final long NON_MAINNET_FEE = 10000L; // 0.0001 ARRR
|
||||
|
||||
private static final Map<ConnectionType, Integer> DEFAULT_LITEWALLET_PORTS = new EnumMap<>(ConnectionType.class);
|
||||
static {
|
||||
DEFAULT_LITEWALLET_PORTS.put(ConnectionType.TCP, 9067);
|
||||
DEFAULT_LITEWALLET_PORTS.put(ConnectionType.SSL, 443);
|
||||
}
|
||||
|
||||
public enum PirateChainNet {
|
||||
MAIN {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return PirateChainMainNetParams.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
new Server("arrrlightd.qortal.online", ConnectionType.SSL, 443),
|
||||
new Server("arrrlightd1.qortal.online", ConnectionType.SSL, 443),
|
||||
new Server("arrrlightd2.qortal.online", ConnectionType.SSL, 443),
|
||||
new Server("lightd.pirate.black", ConnectionType.SSL, 443));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGenesisHash() {
|
||||
return "027e3758c3a65b12aa1046462b486d0a63bfa1beae327897f56c5cfb7daaae71";
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
// TODO: This will need to be replaced with something better in the near future!
|
||||
return MAINNET_FEE;
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return LitecoinTestNet3Params.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGenesisHash() {
|
||||
return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return NON_MAINNET_FEE;
|
||||
}
|
||||
},
|
||||
REGTEST {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return LitecoinRegTestParams.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(
|
||||
new Server("localhost", ConnectionType.TCP, 9067),
|
||||
new Server("localhost", ConnectionType.SSL, 443));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGenesisHash() {
|
||||
// This is unique to each regtest instance
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return NON_MAINNET_FEE;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
public abstract Collection<Server> getServers();
|
||||
public abstract String getGenesisHash();
|
||||
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
|
||||
}
|
||||
|
||||
private static PirateChain instance;
|
||||
|
||||
private final PirateChainNet pirateChainNet;
|
||||
|
||||
// Constructors and instance
|
||||
|
||||
private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
this.pirateChainNet = pirateChainNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name()));
|
||||
}
|
||||
|
||||
public static synchronized PirateChain getInstance() {
|
||||
if (instance == null) {
|
||||
PirateChainNet pirateChainNet = Settings.getInstance().getPirateChainNet();
|
||||
|
||||
BitcoinyBlockchainProvider pirateLightClient = new PirateLightClient("PirateChain-" + pirateChainNet.name(), pirateChainNet.getGenesisHash(), pirateChainNet.getServers(), DEFAULT_LITEWALLET_PORTS);
|
||||
Context bitcoinjContext = new Context(pirateChainNet.getParams());
|
||||
|
||||
instance = new PirateChain(pirateChainNet, pirateLightClient, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
pirateLightClient.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Getters & setters
|
||||
|
||||
public static synchronized void resetForTesting() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns estimated LTC fee, in sats per 1000bytes, optionally for historic timestamp.
|
||||
*
|
||||
* @param timestamp optional milliseconds since epoch, or null for 'now'
|
||||
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
|
||||
*/
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
|
||||
return this.pirateChainNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns confirmed balance, based on passed payment script.
|
||||
* <p>
|
||||
* @return confirmed balance, or zero if balance unknown
|
||||
* @throws ForeignBlockchainException if there was an error
|
||||
*/
|
||||
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
|
||||
return this.blockchainProvider.getConfirmedAddressBalance(base58Address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns median timestamp from latest 11 blocks, in seconds.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public int getMedianBlockTime() throws ForeignBlockchainException {
|
||||
int height = this.blockchainProvider.getCurrentHeight();
|
||||
|
||||
// Grab latest 11 blocks
|
||||
List<Long> blockTimestamps = this.blockchainProvider.getBlockTimestamps(height - 11, 11);
|
||||
if (blockTimestamps.size() < 11)
|
||||
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
|
||||
|
||||
// Descending order
|
||||
blockTimestamps.sort((a, b) -> Long.compare(b, a));
|
||||
|
||||
// Pick median
|
||||
return Math.toIntExact(blockTimestamps.get(5));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of compact blocks
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
public List<CompactFormats.CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
|
||||
return this.blockchainProvider.getCompactBlocks(startHeight, count);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isValidAddress(String address) {
|
||||
// Start with some simple checks
|
||||
if (address == null || !address.toLowerCase().startsWith("zs") || address.length() != 78) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now try Bech32 decoding the address (which includes checksum verification)
|
||||
try {
|
||||
Bech32.Bech32Data decoded = Bech32.decode(address);
|
||||
return (decoded != null && Objects.equals("zs", decoded.hrp));
|
||||
}
|
||||
catch (AddressFormatException e) {
|
||||
// Invalid address, checksum failed, etc
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidWalletKey(String walletKey) {
|
||||
// For Pirate Chain, we only care that the key is a random string
|
||||
// 32 characters in length, as it is used as entropy for the seed.
|
||||
return walletKey != null && Base58.decode(walletKey).length == 32;
|
||||
}
|
||||
|
||||
/** Returns 't3' prefixed P2SH address using passed redeem script. */
|
||||
public String deriveP2shAddress(byte[] redeemScriptBytes) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
return LegacyZcashAddress.fromScriptHash(this.params, redeemScriptHash).toString();
|
||||
}
|
||||
|
||||
/** Returns 'b' prefixed P2SH address using passed redeem script. */
|
||||
public String deriveP2shAddressBPrefix(byte[] redeemScriptBytes) {
|
||||
Context.propagate(bitcoinjContext);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
return LegacyAddress.fromScriptHash(this.params, redeemScriptHash).toString();
|
||||
}
|
||||
|
||||
public Long getWalletBalance(String entropy58) throws ForeignBlockchainException {
|
||||
synchronized (this) {
|
||||
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||
walletController.initWithEntropy58(entropy58);
|
||||
walletController.ensureInitialized();
|
||||
walletController.ensureSynchronized();
|
||||
walletController.ensureNotNullSeed();
|
||||
|
||||
// Get balance
|
||||
String response = LiteWalletJni.execute("balance", "");
|
||||
JSONObject json = new JSONObject(response);
|
||||
if (json.has("zbalance")) {
|
||||
return json.getLong("zbalance");
|
||||
}
|
||||
|
||||
throw new ForeignBlockchainException("Unable to determine balance");
|
||||
}
|
||||
}
|
||||
|
||||
public List<SimpleTransaction> getWalletTransactions(String entropy58) throws ForeignBlockchainException {
|
||||
synchronized (this) {
|
||||
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||
walletController.initWithEntropy58(entropy58);
|
||||
walletController.ensureInitialized();
|
||||
walletController.ensureSynchronized();
|
||||
walletController.ensureNotNullSeed();
|
||||
|
||||
List<SimpleTransaction> transactions = new ArrayList<>();
|
||||
|
||||
// Get transactions list
|
||||
String response = LiteWalletJni.execute("list", "");
|
||||
JSONArray transactionsJson = new JSONArray(response);
|
||||
if (transactionsJson != null) {
|
||||
for (int i = 0; i < transactionsJson.length(); i++) {
|
||||
JSONObject transactionJson = transactionsJson.getJSONObject(i);
|
||||
|
||||
if (transactionJson.has("txid")) {
|
||||
String txId = transactionJson.getString("txid");
|
||||
Long timestamp = transactionJson.getLong("datetime");
|
||||
Long amount = transactionJson.getLong("amount");
|
||||
Long fee = transactionJson.getLong("fee");
|
||||
String memo = null;
|
||||
|
||||
if (transactionJson.has("incoming_metadata")) {
|
||||
JSONArray incomingMetadatas = transactionJson.getJSONArray("incoming_metadata");
|
||||
if (incomingMetadatas != null) {
|
||||
for (int j = 0; j < incomingMetadatas.length(); j++) {
|
||||
JSONObject incomingMetadata = incomingMetadatas.getJSONObject(j);
|
||||
if (incomingMetadata.has("value")) {
|
||||
//String address = incomingMetadata.getString("address");
|
||||
Long value = incomingMetadata.getLong("value");
|
||||
amount = value; // TODO: figure out how to parse transactions with multiple incomingMetadata entries
|
||||
}
|
||||
|
||||
if (incomingMetadata.has("memo") && !incomingMetadata.isNull("memo")) {
|
||||
memo = incomingMetadata.getString("memo");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (transactionJson.has("outgoing_metadata")) {
|
||||
JSONArray outgoingMetadatas = transactionJson.getJSONArray("outgoing_metadata");
|
||||
for (int j = 0; j < outgoingMetadatas.length(); j++) {
|
||||
JSONObject outgoingMetadata = outgoingMetadatas.getJSONObject(j);
|
||||
|
||||
if (outgoingMetadata.has("memo") && !outgoingMetadata.isNull("memo")) {
|
||||
memo = outgoingMetadata.getString("memo");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long timestampMillis = Math.toIntExact(timestamp) * 1000L;
|
||||
SimpleTransaction transaction = new SimpleTransaction(txId, timestampMillis, amount, fee, null, null, memo);
|
||||
transactions.add(transaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
}
|
||||
|
||||
public String getWalletAddress(String entropy58) throws ForeignBlockchainException {
|
||||
synchronized (this) {
|
||||
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||
walletController.initWithEntropy58(entropy58);
|
||||
walletController.ensureInitialized();
|
||||
walletController.ensureNotNullSeed();
|
||||
|
||||
return walletController.getCurrentWallet().getWalletAddress();
|
||||
}
|
||||
}
|
||||
|
||||
public String sendCoins(PirateChainSendRequest pirateChainSendRequest) throws ForeignBlockchainException {
|
||||
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||
walletController.initWithEntropy58(pirateChainSendRequest.entropy58);
|
||||
walletController.ensureInitialized();
|
||||
walletController.ensureSynchronized();
|
||||
walletController.ensureNotNullSeed();
|
||||
|
||||
// Unlock wallet
|
||||
walletController.getCurrentWallet().unlock();
|
||||
|
||||
// Build spend
|
||||
JSONObject txn = new JSONObject();
|
||||
txn.put("input", walletController.getCurrentWallet().getWalletAddress());
|
||||
txn.put("fee", MAINNET_FEE);
|
||||
|
||||
JSONObject output = new JSONObject();
|
||||
output.put("address", pirateChainSendRequest.receivingAddress);
|
||||
output.put("amount", pirateChainSendRequest.arrrAmount);
|
||||
output.put("memo", pirateChainSendRequest.memo);
|
||||
|
||||
JSONArray outputs = new JSONArray();
|
||||
outputs.put(output);
|
||||
txn.put("output", outputs);
|
||||
|
||||
String txnString = txn.toString();
|
||||
|
||||
// Send the coins
|
||||
String response = LiteWalletJni.execute("send", txnString);
|
||||
JSONObject json = new JSONObject(response);
|
||||
try {
|
||||
if (json.has("txid")) { // Success
|
||||
return json.getString("txid");
|
||||
}
|
||||
else if (json.has("error")) {
|
||||
String error = json.getString("error");
|
||||
throw new ForeignBlockchainException(error);
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
throw new ForeignBlockchainException(e.getMessage());
|
||||
}
|
||||
|
||||
throw new ForeignBlockchainException("Something went wrong");
|
||||
}
|
||||
|
||||
public String fundP2SH(String entropy58, String receivingAddress, long amount,
|
||||
String redeemScript58) throws ForeignBlockchainException {
|
||||
|
||||
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||
walletController.initWithEntropy58(entropy58);
|
||||
walletController.ensureInitialized();
|
||||
walletController.ensureSynchronized();
|
||||
walletController.ensureNotNullSeed();
|
||||
|
||||
// Unlock wallet
|
||||
walletController.getCurrentWallet().unlock();
|
||||
|
||||
// Build spend
|
||||
JSONObject txn = new JSONObject();
|
||||
txn.put("input", walletController.getCurrentWallet().getWalletAddress());
|
||||
txn.put("fee", MAINNET_FEE);
|
||||
|
||||
JSONObject output = new JSONObject();
|
||||
output.put("address", receivingAddress);
|
||||
output.put("amount", amount);
|
||||
//output.put("memo", memo);
|
||||
|
||||
JSONArray outputs = new JSONArray();
|
||||
outputs.put(output);
|
||||
txn.put("output", outputs);
|
||||
txn.put("script", redeemScript58);
|
||||
|
||||
String txnString = txn.toString();
|
||||
|
||||
// Send the coins
|
||||
String response = LiteWalletJni.execute("sendp2sh", txnString);
|
||||
JSONObject json = new JSONObject(response);
|
||||
try {
|
||||
if (json.has("txid")) { // Success
|
||||
return json.getString("txid");
|
||||
}
|
||||
else if (json.has("error")) {
|
||||
String error = json.getString("error");
|
||||
throw new ForeignBlockchainException(error);
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
throw new ForeignBlockchainException(e.getMessage());
|
||||
}
|
||||
|
||||
throw new ForeignBlockchainException("Something went wrong");
|
||||
}
|
||||
|
||||
public String redeemP2sh(String p2shAddress, String receivingAddress, long amount, String redeemScript58,
|
||||
String fundingTxid58, String secret58, String privateKey58) throws ForeignBlockchainException {
|
||||
|
||||
// Use null seed wallet since we may not have the entropy bytes for a real wallet's seed
|
||||
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||
walletController.initNullSeedWallet();
|
||||
walletController.ensureInitialized();
|
||||
|
||||
walletController.getCurrentWallet().unlock();
|
||||
|
||||
// Build spend
|
||||
JSONObject txn = new JSONObject();
|
||||
txn.put("input", p2shAddress);
|
||||
txn.put("fee", MAINNET_FEE);
|
||||
|
||||
JSONObject output = new JSONObject();
|
||||
output.put("address", receivingAddress);
|
||||
output.put("amount", amount);
|
||||
// output.put("memo", ""); // Maybe useful in future to include trade details?
|
||||
|
||||
JSONArray outputs = new JSONArray();
|
||||
outputs.put(output);
|
||||
txn.put("output", outputs);
|
||||
|
||||
txn.put("script", redeemScript58);
|
||||
txn.put("txid", fundingTxid58);
|
||||
txn.put("locktime", 0); // Must be 0 when redeeming
|
||||
txn.put("secret", secret58);
|
||||
txn.put("privkey", privateKey58);
|
||||
|
||||
String txnString = txn.toString();
|
||||
|
||||
// Redeem the P2SH
|
||||
String response = LiteWalletJni.execute("redeemp2sh", txnString);
|
||||
JSONObject json = new JSONObject(response);
|
||||
try {
|
||||
if (json.has("txid")) { // Success
|
||||
return json.getString("txid");
|
||||
}
|
||||
else if (json.has("error")) {
|
||||
String error = json.getString("error");
|
||||
throw new ForeignBlockchainException(error);
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
throw new ForeignBlockchainException(e.getMessage());
|
||||
}
|
||||
|
||||
throw new ForeignBlockchainException("Something went wrong");
|
||||
}
|
||||
|
||||
public String refundP2sh(String p2shAddress, String receivingAddress, long amount, String redeemScript58,
|
||||
String fundingTxid58, int lockTime, String privateKey58) throws ForeignBlockchainException {
|
||||
|
||||
// Use null seed wallet since we may not have the entropy bytes for a real wallet's seed
|
||||
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||
walletController.initNullSeedWallet();
|
||||
walletController.ensureInitialized();
|
||||
|
||||
walletController.getCurrentWallet().unlock();
|
||||
|
||||
// Build spend
|
||||
JSONObject txn = new JSONObject();
|
||||
txn.put("input", p2shAddress);
|
||||
txn.put("fee", MAINNET_FEE);
|
||||
|
||||
JSONObject output = new JSONObject();
|
||||
output.put("address", receivingAddress);
|
||||
output.put("amount", amount);
|
||||
// output.put("memo", ""); // Maybe useful in future to include trade details?
|
||||
|
||||
JSONArray outputs = new JSONArray();
|
||||
outputs.put(output);
|
||||
txn.put("output", outputs);
|
||||
|
||||
txn.put("script", redeemScript58);
|
||||
txn.put("txid", fundingTxid58);
|
||||
txn.put("locktime", lockTime);
|
||||
txn.put("secret", ""); // Must be blank when refunding
|
||||
txn.put("privkey", privateKey58);
|
||||
|
||||
String txnString = txn.toString();
|
||||
|
||||
// Redeem the P2SH
|
||||
String response = LiteWalletJni.execute("redeemp2sh", txnString);
|
||||
JSONObject json = new JSONObject(response);
|
||||
try {
|
||||
if (json.has("txid")) { // Success
|
||||
return json.getString("txid");
|
||||
}
|
||||
else if (json.has("error")) {
|
||||
String error = json.getString("error");
|
||||
throw new ForeignBlockchainException(error);
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
throw new ForeignBlockchainException(e.getMessage());
|
||||
}
|
||||
|
||||
throw new ForeignBlockchainException("Something went wrong");
|
||||
}
|
||||
|
||||
public String getSyncStatus(String entropy58) throws ForeignBlockchainException {
|
||||
synchronized (this) {
|
||||
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||
walletController.initWithEntropy58(entropy58);
|
||||
|
||||
return walletController.getSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
public static BitcoinyTransaction deserializeRawTransaction(String rawTransactionHex) throws TransformationException {
|
||||
byte[] rawTransactionData = HashCode.fromString(rawTransactionHex).asBytes();
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(rawTransactionData);
|
||||
|
||||
// Header
|
||||
int header = BitTwiddling.readU32(byteBuffer);
|
||||
boolean overwintered = ((header >> 31 & 0xff) == 255);
|
||||
int version = header & 0x7FFFFFFF;
|
||||
|
||||
// Version group ID
|
||||
int versionGroupId = 0;
|
||||
if (overwintered) {
|
||||
versionGroupId = BitTwiddling.readU32(byteBuffer);
|
||||
}
|
||||
|
||||
boolean isOverwinterV3 = overwintered && versionGroupId == 0x03C48270 && version == 3;
|
||||
boolean isSaplingV4 = overwintered && versionGroupId == 0x892F2085 && version == 4;
|
||||
if (overwintered && !(isOverwinterV3 || isSaplingV4)) {
|
||||
throw new TransformationException("Unknown transaction format");
|
||||
}
|
||||
|
||||
// Inputs
|
||||
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
|
||||
int vinCount = BitTwiddling.readU8(byteBuffer);
|
||||
for (int i=0; i<vinCount; i++) {
|
||||
// Outpoint hash
|
||||
byte[] outpointHashBytes = new byte[32];
|
||||
byteBuffer.get(outpointHashBytes);
|
||||
String outpointHash = HashCode.fromBytes(outpointHashBytes).toString();
|
||||
|
||||
// vout
|
||||
int vout = BitTwiddling.readU32(byteBuffer);
|
||||
|
||||
// scriptSig
|
||||
int scriptSigLength = BitTwiddling.readU8(byteBuffer);
|
||||
byte[] scriptSigBytes = new byte[scriptSigLength];
|
||||
byteBuffer.get(scriptSigBytes);
|
||||
String scriptSig = HashCode.fromBytes(scriptSigBytes).toString();
|
||||
|
||||
int sequence = BitTwiddling.readU32(byteBuffer);
|
||||
|
||||
BitcoinyTransaction.Input input = new BitcoinyTransaction.Input(scriptSig, sequence, outpointHash, vout);
|
||||
inputs.add(input);
|
||||
}
|
||||
|
||||
// Outputs
|
||||
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
|
||||
int voutCount = BitTwiddling.readU8(byteBuffer);
|
||||
for (int i=0; i<voutCount; i++) {
|
||||
// Amount
|
||||
byte[] amountBytes = new byte[8];
|
||||
byteBuffer.get(amountBytes);
|
||||
long amount = BitTwiddling.longFromLEBytes(amountBytes, 0);
|
||||
|
||||
// Script pubkey
|
||||
int scriptPubkeySize = BitTwiddling.readU8(byteBuffer);
|
||||
byte[] scriptPubkeyBytes = new byte[scriptPubkeySize];
|
||||
byteBuffer.get(scriptPubkeyBytes);
|
||||
String scriptPubKey = HashCode.fromBytes(scriptPubkeyBytes).toString();
|
||||
|
||||
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, amount, null));
|
||||
}
|
||||
|
||||
// Locktime
|
||||
byte[] locktimeBytes = new byte[4];
|
||||
byteBuffer.get(locktimeBytes);
|
||||
int locktime = BitTwiddling.intFromLEBytes(locktimeBytes, 0);
|
||||
|
||||
// Expiry height
|
||||
int expiryHeight = 0;
|
||||
if (isOverwinterV3 || isSaplingV4) {
|
||||
byte[] expiryHeightBytes = new byte[4];
|
||||
byteBuffer.get(expiryHeightBytes);
|
||||
expiryHeight = BitTwiddling.intFromLEBytes(expiryHeightBytes, 0);
|
||||
}
|
||||
|
||||
String txHash = null; // Not present in raw transaction data
|
||||
int size = 0; // Not present in raw transaction data
|
||||
Integer timestamp = null; // Not present in raw transaction data
|
||||
|
||||
// Note: this is incomplete, as sapling spend info is not yet parsed. We don't need it for our
|
||||
// current trade bot implementation, but it could be added in the future, for completeness.
|
||||
// See link below for reference:
|
||||
// https://github.com/PirateNetwork/librustzcash/blob/2981c4d2860f7cd73282fed885daac0323ff0280/zcash_primitives/src/transaction/mod.rs#L197
|
||||
|
||||
return new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
|
||||
}
|
||||
|
||||
}
|
||||
875
src/main/java/org/qortal/crosschain/PirateChainACCTv3.java
Normal file
875
src/main/java/org/qortal/crosschain/PirateChainACCTv3.java
Normal file
@@ -0,0 +1,875 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
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.transaction.MessageTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
/**
|
||||
* Cross-chain trade AT
|
||||
*
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Bob generates PirateChain & Qortal 'trade' keys
|
||||
* <ul>
|
||||
* <li>private key required to sign P2SH redeem tx</li>
|
||||
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li>
|
||||
* <li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Bob deploys Qortal AT
|
||||
* <ul>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Alice finds Qortal AT and wants to trade
|
||||
* <ul>
|
||||
* <li>Alice generates PirateChain & Qortal 'trade' keys</li>
|
||||
* <li>Alice funds PirateChain P2SH-A</li>
|
||||
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
|
||||
* <ul>
|
||||
* <li>hash-of-secret-A</li>
|
||||
* <li>her 'trade' Pirate Chain public key</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Bob receives "offer" MESSAGE
|
||||
* <ul>
|
||||
* <li>Checks Alice's P2SH-A</li>
|
||||
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
|
||||
* <ul>
|
||||
* <li>Alice's trade Qortal address</li>
|
||||
* <li>Alice's trade Pirate Chain public key</li>
|
||||
* <li>hash-of-secret-A</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Alice checks Qortal AT to confirm it's locked to her
|
||||
* <ul>
|
||||
* <li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
|
||||
* <ul>
|
||||
* <li>secret-A</li>
|
||||
* <li>Qortal receiving address of her chosing</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>AT's QORT funds are sent to Qortal receiving address</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Bob checks AT, extracts secret-A
|
||||
* <ul>
|
||||
* <li>Bob redeems P2SH-A using his PirateChain trade key and secret-A</li>
|
||||
* <li>P2SH-A ARRR funds end up at PirateChain address determined by redeem transaction output(s)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public class PirateChainACCTv3 implements ACCT {
|
||||
|
||||
public static final String NAME = PirateChainACCTv3.class.getSimpleName();
|
||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("fc2818ac0819ab658a065ab0d050e75f167921e2dce5969b9b7741e47e477d83").asBytes(); // SHA256 of AT code bytes
|
||||
|
||||
public static final int SECRET_LENGTH = 32;
|
||||
|
||||
/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
|
||||
private static final int MODE_VALUE_OFFSET = 68;
|
||||
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */
|
||||
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
|
||||
|
||||
public static class OfferMessageData {
|
||||
public byte[] partnerPirateChainPublicKey;
|
||||
public byte[] hashOfSecretA;
|
||||
public long lockTimeA;
|
||||
}
|
||||
public static final int OFFER_MESSAGE_LENGTH = 33 /*partnerPirateChainPublicKey*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
|
||||
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
|
||||
+ 40 /*partner's Pirate Chain public key (padded from 33 to 40)*/
|
||||
+ 8 /*AT trade timeout (minutes)*/
|
||||
+ 24 /*hash of secret-A (padded from 20 to 24)*/
|
||||
+ 8 /*lockTimeA*/;
|
||||
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
|
||||
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
|
||||
|
||||
private static PirateChainACCTv3 instance;
|
||||
|
||||
private PirateChainACCTv3() {
|
||||
}
|
||||
|
||||
public static synchronized PirateChainACCTv3 getInstance() {
|
||||
if (instance == null)
|
||||
instance = new PirateChainACCTv3();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCodeBytesHash() {
|
||||
return CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModeByteOffset() {
|
||||
return MODE_BYTE_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignBlockchain getBlockchain() {
|
||||
return PirateChain.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Qortal AT creation bytes for cross-chain trading AT.
|
||||
* <p>
|
||||
* <tt>tradeTimeout</tt> (minutes) is the time window for the trade partner to send the
|
||||
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
|
||||
*
|
||||
* @param creatorTradeAddress AT creator's trade Qortal address
|
||||
* @param pirateChainPublicKeyHash 33-byte creator's trade PirateChain public key
|
||||
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
||||
* @param arrrAmount how much ARRR the AT creator is expecting to trade
|
||||
* @param tradeTimeout suggested timeout for entire trade
|
||||
*/
|
||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] pirateChainPublicKeyHash, long qortAmount, long arrrAmount, int tradeTimeout) {
|
||||
if (pirateChainPublicKeyHash.length != 33)
|
||||
throw new IllegalArgumentException("PirateChain public key hash should be 33 bytes");
|
||||
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
// Constants (with corresponding dataByteBuffer.put*() calls below)
|
||||
|
||||
final int addrCreatorTradeAddress1 = addrCounter++;
|
||||
final int addrCreatorTradeAddress2 = addrCounter++;
|
||||
final int addrCreatorTradeAddress3 = addrCounter++;
|
||||
final int addrCreatorTradeAddress4 = addrCounter++;
|
||||
|
||||
final int addrPirateChainPublicKeyHash = addrCounter;
|
||||
addrCounter += 5;
|
||||
|
||||
final int addrQortAmount = addrCounter++;
|
||||
final int addrarrrAmount = addrCounter++;
|
||||
final int addrTradeTimeout = addrCounter++;
|
||||
|
||||
final int addrMessageTxnType = addrCounter++;
|
||||
final int addrExpectedTradeMessageLength = addrCounter++;
|
||||
final int addrExpectedRedeemMessageLength = addrCounter++;
|
||||
|
||||
final int addrCreatorAddressPointer = addrCounter++;
|
||||
final int addrQortalPartnerAddressPointer = addrCounter++;
|
||||
final int addrMessageSenderPointer = addrCounter++;
|
||||
|
||||
final int addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset = addrCounter++;
|
||||
final int addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset = addrCounter++; // Remainder of public key, plus timeout
|
||||
final int addrPartnerPirateChainPublicKeyFirst32BytesPointer = addrCounter++;
|
||||
final int addrPartnerPirateChainPublicKeyLastBytePointer = addrCounter++; // Remainder of public key
|
||||
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
|
||||
final int addrHashOfSecretAPointer = addrCounter++;
|
||||
|
||||
final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
|
||||
|
||||
final int addrMessageDataPointer = addrCounter++;
|
||||
final int addrMessageDataLength = addrCounter++;
|
||||
|
||||
final int addrPartnerReceivingAddressPointer = addrCounter++;
|
||||
|
||||
final int addrEndOfConstants = addrCounter;
|
||||
|
||||
// Variables
|
||||
|
||||
final int addrCreatorAddress1 = addrCounter++;
|
||||
final int addrCreatorAddress2 = addrCounter++;
|
||||
final int addrCreatorAddress3 = addrCounter++;
|
||||
final int addrCreatorAddress4 = addrCounter++;
|
||||
|
||||
final int addrQortalPartnerAddress1 = addrCounter++;
|
||||
final int addrQortalPartnerAddress2 = addrCounter++;
|
||||
final int addrQortalPartnerAddress3 = addrCounter++;
|
||||
final int addrQortalPartnerAddress4 = addrCounter++;
|
||||
|
||||
final int addrLockTimeA = addrCounter++;
|
||||
final int addrRefundTimeout = addrCounter++;
|
||||
final int addrRefundTimestamp = addrCounter++;
|
||||
final int addrLastTxnTimestamp = addrCounter++;
|
||||
final int addrBlockTimestamp = addrCounter++;
|
||||
final int addrTxnType = addrCounter++;
|
||||
final int addrResult = addrCounter++;
|
||||
|
||||
final int addrMessageSender1 = addrCounter++;
|
||||
final int addrMessageSender2 = addrCounter++;
|
||||
final int addrMessageSender3 = addrCounter++;
|
||||
final int addrMessageSender4 = addrCounter++;
|
||||
|
||||
final int addrMessageLength = addrCounter++;
|
||||
|
||||
final int addrMessageData = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrHashOfSecretA = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrPartnerPirateChainPublicKeyFirst32Bytes = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrPartnerPirateChainPublicKeyLastByte = addrCounter;
|
||||
addrCounter += 4; // We retrieve using GET_B_IND, so need to allow space for the full 32 bytes
|
||||
|
||||
final int addrPartnerReceivingAddress = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrMode = addrCounter++;
|
||||
assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET);
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
// AT creator's trade Qortal address, decoded from Base58
|
||||
assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
|
||||
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
|
||||
|
||||
// PirateChain public key hash
|
||||
assert dataByteBuffer.position() == addrPirateChainPublicKeyHash * MachineState.VALUE_SIZE : "addrPirateChainPublicKeyHash incorrect";
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(pirateChainPublicKeyHash, 40, 0));
|
||||
|
||||
// Redeem Qort amount
|
||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||
dataByteBuffer.putLong(qortAmount);
|
||||
|
||||
// Expected PirateChain amount
|
||||
assert dataByteBuffer.position() == addrarrrAmount * MachineState.VALUE_SIZE : "addrarrrAmount incorrect";
|
||||
dataByteBuffer.putLong(arrrAmount);
|
||||
|
||||
// Suggested trade timeout (minutes)
|
||||
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
|
||||
dataByteBuffer.putLong(tradeTimeout);
|
||||
|
||||
// We're only interested in MESSAGE transactions
|
||||
assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
|
||||
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
|
||||
|
||||
// Expected length of 'trade' MESSAGE data from AT creator
|
||||
assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
|
||||
dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
|
||||
|
||||
// Expected length of 'redeem' MESSAGE data from trade partner
|
||||
assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
|
||||
dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
|
||||
|
||||
// Index into data segment of AT creator's address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
|
||||
dataByteBuffer.putLong(addrCreatorAddress1);
|
||||
|
||||
// Index into data segment of partner's Qortal address, used by SET_B_IND
|
||||
assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
|
||||
dataByteBuffer.putLong(addrQortalPartnerAddress1);
|
||||
|
||||
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
|
||||
dataByteBuffer.putLong(addrMessageSender1);
|
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting first 32 bytes of partner's Pirate Chain public key
|
||||
assert dataByteBuffer.position() == addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset incorrect";
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting last byte of public key
|
||||
assert dataByteBuffer.position() == addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset incorrect";
|
||||
dataByteBuffer.putLong(64L);
|
||||
|
||||
// Index into data segment of partner's Pirate Chain public key, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerPirateChainPublicKeyFirst32BytesPointer * MachineState.VALUE_SIZE : "addrPartnerPirateChainPublicKeyFirst32BytesPointer incorrect";
|
||||
dataByteBuffer.putLong(addrPartnerPirateChainPublicKeyFirst32Bytes);
|
||||
|
||||
// Index into data segment of remainder of partner's Pirate Chain public key, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerPirateChainPublicKeyLastBytePointer * MachineState.VALUE_SIZE : "addrPartnerPirateChainPublicKeyLastBytePointer incorrect";
|
||||
dataByteBuffer.putLong(addrPartnerPirateChainPublicKeyLastByte);
|
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
||||
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
|
||||
dataByteBuffer.putLong(80L);
|
||||
|
||||
// Index into data segment to hash of secret A, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
|
||||
dataByteBuffer.putLong(addrHashOfSecretA);
|
||||
|
||||
// Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
|
||||
assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
// Source location and length for hashing any passed secret
|
||||
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
|
||||
dataByteBuffer.putLong(addrMessageData);
|
||||
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
// Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
|
||||
dataByteBuffer.putLong(addrPartnerReceivingAddress);
|
||||
|
||||
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
|
||||
|
||||
// Code labels
|
||||
Integer labelRefund = null;
|
||||
|
||||
Integer labelTradeTxnLoop = null;
|
||||
Integer labelCheckTradeTxn = null;
|
||||
Integer labelCheckCancelTxn = null;
|
||||
Integer labelNotTradeNorCancelTxn = null;
|
||||
Integer labelCheckNonRefundTradeTxn = null;
|
||||
Integer labelTradeTxnExtract = null;
|
||||
Integer labelRedeemTxnLoop = null;
|
||||
Integer labelCheckRedeemTxn = null;
|
||||
Integer labelCheckRedeemTxnSender = null;
|
||||
Integer labelPayout = null;
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
|
||||
|
||||
// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
|
||||
|
||||
/* Transaction processing loop */
|
||||
labelTradeTxnLoop = codeByteBuffer.position();
|
||||
|
||||
/* Sleep until message arrives */
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
|
||||
|
||||
// Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
|
||||
// If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
/* Check transaction */
|
||||
labelCheckTradeTxn = codeByteBuffer.position();
|
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
|
||||
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
|
||||
// If transaction type is not MESSAGE type then go look for another transaction
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
|
||||
|
||||
/* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
|
||||
|
||||
// Extract sender address from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
|
||||
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
|
||||
// Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
|
||||
// Message sender's address matches AT creator's trade address so go process 'trade' message
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
|
||||
|
||||
/* Checking message sender for possible cancel message */
|
||||
labelCheckCancelTxn = codeByteBuffer.position();
|
||||
|
||||
// Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
|
||||
// Partner address is AT creator's address, so cancel offer and finish.
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
|
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||
|
||||
/* Not trade nor cancel message */
|
||||
labelNotTradeNorCancelTxn = codeByteBuffer.position();
|
||||
|
||||
// Loop to find another transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
|
||||
|
||||
/* Possible switch-to-trade-mode message */
|
||||
labelCheckNonRefundTradeTxn = codeByteBuffer.position();
|
||||
|
||||
// Check 'trade' message we received has expected number of message bytes
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
|
||||
// If message length matches, branch to info extraction code
|
||||
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
|
||||
// Message length didn't match - go back to finding another 'trade' MESSAGE transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
|
||||
|
||||
/* Extracting info from 'trade' MESSAGE transaction */
|
||||
labelTradeTxnExtract = codeByteBuffer.position();
|
||||
|
||||
// Extract message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
|
||||
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
|
||||
|
||||
// Extract first 32 bytes of trade partner's Pirate Chain public key from message into B
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset));
|
||||
// Store first 32 bytes of partner's Pirate Chain public key
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerPirateChainPublicKeyFirst32BytesPointer));
|
||||
|
||||
// Extract last byte of public key, plus trade timeout, from message into B
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset));
|
||||
// Store last byte of partner's Pirate Chain public key
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerPirateChainPublicKeyLastBytePointer));
|
||||
// Extract AT trade timeout (minutes) (from B2)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B2, addrRefundTimeout));
|
||||
|
||||
// Grab next 32 bytes
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
|
||||
|
||||
// Extract hash-of-secret-A (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
|
||||
// Extract lockTime-A (from B4)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
|
||||
|
||||
// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
|
||||
|
||||
/* We are in 'trade mode' */
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
|
||||
|
||||
// Fetch current block 'timestamp'
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
|
||||
// If we're not past refund 'timestamp' then look for next transaction
|
||||
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||
// We're past refund 'timestamp' so go refund everything back to AT creator
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
|
||||
|
||||
/* Transaction processing loop */
|
||||
labelRedeemTxnLoop = codeByteBuffer.position();
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
|
||||
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
|
||||
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
|
||||
// Stop and wait for next block
|
||||
codeByteBuffer.put(OpCode.STP_IMD.compile());
|
||||
|
||||
/* Check transaction */
|
||||
labelCheckRedeemTxn = codeByteBuffer.position();
|
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
|
||||
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
|
||||
// If transaction type is not MESSAGE type then go look for another transaction
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||
|
||||
/* Check message payload length */
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
|
||||
// If message length matches, branch to sender checking code
|
||||
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
|
||||
// Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
|
||||
|
||||
/* Check transaction's sender */
|
||||
labelCheckRedeemTxnSender = codeByteBuffer.position();
|
||||
|
||||
// Extract sender address from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
|
||||
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
|
||||
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
|
||||
|
||||
/* Check 'secret-A' in transaction's message */
|
||||
|
||||
// Extract secret-A from first 32 bytes of message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
|
||||
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
|
||||
// Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
|
||||
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
|
||||
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
|
||||
// If hashes don't match, addrResult will be zero so go find another transaction
|
||||
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
|
||||
|
||||
/* Success! Pay arranged amount to receiving address */
|
||||
labelPayout = codeByteBuffer.position();
|
||||
|
||||
// Extract Qortal receiving address from next 32 bytes of message from transaction into B register
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
|
||||
// Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
|
||||
// Pay AT's balance to receiving address
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
|
||||
// Set redeemed mode
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
|
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||
|
||||
// Fall-through to refunding any remaining balance back to AT creator
|
||||
|
||||
/* Refund balance back to AT creator */
|
||||
labelRefund = codeByteBuffer.position();
|
||||
|
||||
// Set refunded mode
|
||||
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
|
||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile ARRR-QORT ACCT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
assert Arrays.equals(Crypto.digest(codeBytes), PirateChainACCTv3.CODE_BYTES_HASH)
|
||||
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
@Override
|
||||
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
|
||||
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
|
||||
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CrossChainTradeData with useful info extracted from AT.
|
||||
*/
|
||||
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
|
||||
byte[] addressBytes = new byte[25]; // for general use
|
||||
String atAddress = atStateData.getATAddress();
|
||||
|
||||
CrossChainTradeData tradeData = new CrossChainTradeData();
|
||||
|
||||
tradeData.foreignBlockchain = SupportedBlockchain.PIRATECHAIN.name();
|
||||
tradeData.acctName = NAME;
|
||||
|
||||
tradeData.qortalAtAddress = atAddress;
|
||||
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
|
||||
tradeData.creationTimestamp = creationTimestamp;
|
||||
|
||||
Account atAccount = new Account(repository, atAddress);
|
||||
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
|
||||
dataByteBuffer.position(MachineState.HEADER_LENGTH);
|
||||
|
||||
/* Constants */
|
||||
|
||||
// Skip creator's trade address
|
||||
dataByteBuffer.get(addressBytes);
|
||||
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||
|
||||
// Creator's PirateChain/foreign public key (full 33 bytes, not hashed, so ignore references to "PKH")
|
||||
tradeData.creatorForeignPKH = new byte[33];
|
||||
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 40 - tradeData.creatorForeignPKH.length); // skip to 40 bytes
|
||||
|
||||
// We don't use secret-B
|
||||
tradeData.hashOfSecretB = null;
|
||||
|
||||
// Redeem payout
|
||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Expected ARRR amount
|
||||
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Trade timeout
|
||||
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
||||
|
||||
// Skip MESSAGE transaction type
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip expected 'trade' message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip expected 'redeem' message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to creator's address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to partner's Qortal trade address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to message sender
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip 'trade' message data offset for first 32 bytes of partner's Pirate Chain public key
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip 'trade' message data offset for last 32 byte of partner's Pirate Chain public key
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to partner's Pirate Chain public key (first 32 bytes)
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to partner's Pirate Chain public key (last byte)
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip 'trade' message data offset for hash-of-secret-A
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to hash-of-secret-A
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip 'redeem' message data offset for partner's Qortal receiving address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to message data
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip message data length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to partner's receiving address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
/* End of constants / begin variables */
|
||||
|
||||
// Skip AT creator's address
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
|
||||
|
||||
// Partner's trade address (if present)
|
||||
dataByteBuffer.get(addressBytes);
|
||||
String qortalRecipient = Base58.encode(addressBytes);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||
|
||||
// Potential lockTimeA (if in trade mode)
|
||||
int lockTimeA = (int) dataByteBuffer.getLong();
|
||||
|
||||
// AT refund timeout (probably only useful for debugging)
|
||||
int refundTimeout = (int) dataByteBuffer.getLong();
|
||||
|
||||
// Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
|
||||
long tradeRefundTimestamp = dataByteBuffer.getLong();
|
||||
|
||||
// Skip last transaction timestamp
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip block timestamp
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip transaction type
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip temporary result
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip temporary message sender
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
|
||||
|
||||
// Skip message length
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip temporary message data
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
|
||||
|
||||
// Potential hash160 of secret A
|
||||
byte[] hashOfSecretA = new byte[20];
|
||||
dataByteBuffer.get(hashOfSecretA);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
|
||||
|
||||
// Potential partner's PirateChain public key
|
||||
byte[] partnerPirateChainPublicKey = new byte[33];
|
||||
dataByteBuffer.get(partnerPirateChainPublicKey);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 64 - partnerPirateChainPublicKey.length); // skip to 64 bytes
|
||||
|
||||
// Partner's receiving address (if present)
|
||||
byte[] partnerReceivingAddress = new byte[25];
|
||||
dataByteBuffer.get(partnerReceivingAddress);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
|
||||
|
||||
// Trade AT's 'mode'
|
||||
long modeValue = dataByteBuffer.getLong();
|
||||
AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL));
|
||||
|
||||
/* End of variables */
|
||||
|
||||
if (mode != null && mode != AcctMode.OFFERING) {
|
||||
tradeData.mode = mode;
|
||||
tradeData.refundTimeout = refundTimeout;
|
||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||
tradeData.qortalPartnerAddress = qortalRecipient;
|
||||
tradeData.hashOfSecretA = hashOfSecretA;
|
||||
tradeData.partnerForeignPKH = partnerPirateChainPublicKey; // Not hashed
|
||||
tradeData.lockTimeA = lockTimeA;
|
||||
|
||||
if (mode == AcctMode.REDEEMED)
|
||||
tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
|
||||
} else {
|
||||
tradeData.mode = AcctMode.OFFERING;
|
||||
}
|
||||
|
||||
tradeData.duplicateDeprecated();
|
||||
|
||||
return tradeData;
|
||||
}
|
||||
|
||||
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
|
||||
public static byte[] buildOfferMessage(byte[] partnerBitcoinPublicKey, byte[] hashOfSecretA, int lockTimeA) {
|
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||
return Bytes.concat(partnerBitcoinPublicKey, hashOfSecretA, lockTimeABytes);
|
||||
}
|
||||
|
||||
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
|
||||
public static OfferMessageData extractOfferMessageData(byte[] messageData) {
|
||||
if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
|
||||
return null;
|
||||
|
||||
OfferMessageData offerMessageData = new OfferMessageData();
|
||||
offerMessageData.partnerPirateChainPublicKey = Arrays.copyOfRange(messageData, 0, 33);
|
||||
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 33, 53);
|
||||
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 53);
|
||||
|
||||
return offerMessageData;
|
||||
}
|
||||
|
||||
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
|
||||
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPublicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||
byte[] data = new byte[TRADE_MESSAGE_LENGTH];
|
||||
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
|
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
|
||||
|
||||
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
|
||||
System.arraycopy(partnerBitcoinPublicKey, 0, data, 32, partnerBitcoinPublicKey.length);
|
||||
System.arraycopy(refundTimeoutBytes, 0, data, 72, refundTimeoutBytes.length);
|
||||
System.arraycopy(hashOfSecretA, 0, data, 80, hashOfSecretA.length);
|
||||
System.arraycopy(lockTimeABytes, 0, data, 104, lockTimeABytes.length);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
|
||||
@Override
|
||||
public byte[] buildCancelMessage(String creatorQortalAddress) {
|
||||
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
|
||||
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
|
||||
|
||||
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */
|
||||
public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) {
|
||||
byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
|
||||
byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
|
||||
|
||||
System.arraycopy(secretA, 0, data, 0, secretA.length);
|
||||
System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
|
||||
public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
|
||||
// refund should be triggered halfway between offerMessageTimestamp and lockTimeA
|
||||
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
||||
String atAddress = crossChainTradeData.qortalAtAddress;
|
||||
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
|
||||
|
||||
// We don't have partner's public key so we check every message to AT
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
|
||||
if (messageTransactionsData == null)
|
||||
return null;
|
||||
|
||||
// Find 'redeem' message
|
||||
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
|
||||
// Check message payload type/encryption
|
||||
if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
|
||||
continue;
|
||||
|
||||
// Check message payload size
|
||||
byte[] messageData = messageTransactionData.getData();
|
||||
if (messageData.length != REDEEM_MESSAGE_LENGTH)
|
||||
// Wrong payload length
|
||||
continue;
|
||||
|
||||
// Check sender
|
||||
if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
|
||||
// Wrong sender;
|
||||
continue;
|
||||
|
||||
// Extract secretA
|
||||
byte[] secretA = new byte[32];
|
||||
System.arraycopy(messageData, 0, secretA, 0, secretA.length);
|
||||
|
||||
byte[] hashOfSecretA = Crypto.hash160(secretA);
|
||||
if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
|
||||
continue;
|
||||
|
||||
return secretA;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
412
src/main/java/org/qortal/crosschain/PirateChainHTLC.java
Normal file
412
src/main/java/org/qortal/crosschain/PirateChainHTLC.java
Normal file
@@ -0,0 +1,412 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.core.Transaction.SigHash;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.script.ScriptChunk;
|
||||
import org.bitcoinj.script.ScriptOpCodes;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class PirateChainHTLC {
|
||||
|
||||
public enum Status {
|
||||
UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED
|
||||
}
|
||||
|
||||
public static final int SECRET_LENGTH = 32;
|
||||
public static final int MIN_LOCKTIME = 1500000000;
|
||||
|
||||
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
|
||||
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
|
||||
|
||||
// Assuming node's trade-bot has no more than 100 entries?
|
||||
private static final int MAX_CACHE_ENTRIES = 100;
|
||||
|
||||
// Max time-to-live for cache entries (milliseconds)
|
||||
private static final long CACHE_TIMEOUT = 30_000L;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final Map<String, byte[]> SECRET_CACHE = new LinkedHashMap<>(MAX_CACHE_ENTRIES + 1, 0.75F, true) {
|
||||
// This method is called just after a new entry has been added
|
||||
@Override
|
||||
public boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
|
||||
return size() > MAX_CACHE_ENTRIES;
|
||||
}
|
||||
};
|
||||
private static final byte[] NO_SECRET_CACHE_ENTRY = new byte[0];
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static final Map<String, Status> STATUS_CACHE = new LinkedHashMap<>(MAX_CACHE_ENTRIES + 1, 0.75F, true) {
|
||||
// This method is called just after a new entry has been added
|
||||
@Override
|
||||
public boolean removeEldestEntry(Map.Entry<String, Status> eldest) {
|
||||
return size() > MAX_CACHE_ENTRIES;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* OP_RETURN + OP_PUSHDATA1 + bytes (not part of actual redeem script - used for "push only" secondary output when funding P2SH)
|
||||
*
|
||||
* OP_IF (if top stack value isn't false) (true=refund; false=redeem) (boolean is then removed from stack)
|
||||
* <push 4 bytes> <intended locktime>
|
||||
* OP_CHECKLOCKTIMEVERIFY (if stack locktime greater than transaction's lock time - i.e. refunding but too soon - then fail validation)
|
||||
* OP_DROP (remove locktime from top of stack)
|
||||
* <push 33 bytes> <intended refunder public key>
|
||||
* OP_CHECKSIG (check signature and public key are correct; returns 1 or 0)
|
||||
* OP_ELSE (if top stack value was false, i.e. attempting to redeem)
|
||||
* OP_SIZE (push length of top item - the secret - to the top of the stack)
|
||||
* <push 1 byte> 32
|
||||
* OP_EQUALVERIFY (unhashed secret must be 32 bytes in length)
|
||||
* OP_HASH160 (hash the secret)
|
||||
* <push 20 bytes> <intended secret hash>
|
||||
* OP_EQUALVERIFY (ensure hash of supplied secret matches intended secret hash; transaction invalid if no match)
|
||||
* <push 33 bytes> <intended redeemer public key>
|
||||
* OP_CHECKSIG (check signature and public key are correct; returns 1 or 0)
|
||||
* OP_ENDIF
|
||||
*/
|
||||
|
||||
private static final byte[] pushOnlyPrefix = HashCode.fromString("6a4c").asBytes(); // OP_RETURN + push(redeem script)
|
||||
private static final byte[] redeemScript1 = HashCode.fromString("6304").asBytes(); // OP_IF push(4 bytes locktime)
|
||||
private static final byte[] redeemScript2 = HashCode.fromString("b17521").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_DROP push(33 bytes refund pubkey)
|
||||
private static final byte[] redeemScript3 = HashCode.fromString("ac6782012088a914").asBytes(); // OP_CHECKSIG OP_ELSE OP_SIZE push(0x20) OP_EQUALVERIFY OP_HASH160 push(20 bytes hash of secret)
|
||||
private static final byte[] redeemScript4 = HashCode.fromString("8821").asBytes(); // OP_EQUALVERIFY push(33 bytes redeem pubkey)
|
||||
private static final byte[] redeemScript5 = HashCode.fromString("ac68").asBytes(); // OP_CHECKSIG OP_ENDIF
|
||||
|
||||
/**
|
||||
* Returns redeemScript used for cross-chain trading.
|
||||
* <p>
|
||||
* See comments in {@link PirateChainHTLC} for more details.
|
||||
*
|
||||
* @param refunderPubKey 33-byte P2SH funder's public key, for refunding purposes
|
||||
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||
* @param redeemerPubKey 33-byte P2SH redeemer's public key
|
||||
* @param hashOfSecret 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||
*/
|
||||
public static byte[] buildScript(byte[] refunderPubKey, int lockTime, byte[] redeemerPubKey, byte[] hashOfSecret) {
|
||||
return Bytes.concat(redeemScript1, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), redeemScript2,
|
||||
refunderPubKey, redeemScript3, hashOfSecret, redeemScript4, redeemerPubKey, redeemScript5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to buildScript() above, this time with a prefix suitable for adding the redeem script
|
||||
* to a "push only" output (via OP_RETURN followed by OP_PUSHDATA1)
|
||||
*
|
||||
* @param refunderPubKey 33-byte P2SH funder's public key, for refunding purposes
|
||||
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||
* @param redeemerPubKey 33-byte P2SH redeemer's public key
|
||||
* @param hashOfSecret 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||
* @return
|
||||
*/
|
||||
public static byte[] buildScriptWithPrefix(byte[] refunderPubKey, int lockTime, byte[] redeemerPubKey, byte[] hashOfSecret) {
|
||||
byte[] redeemScript = buildScript(refunderPubKey, lockTime, redeemerPubKey, hashOfSecret);
|
||||
int size = redeemScript.length;
|
||||
String sizeHex = Integer.toHexString(size & 0xFF);
|
||||
return Bytes.concat(pushOnlyPrefix, HashCode.fromString(sizeHex).asBytes(), redeemScript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 'secret', if any, given HTLC's P2SH address.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
public static byte[] findHtlcSecret(Bitcoiny bitcoiny, String p2shAddress) throws ForeignBlockchainException {
|
||||
NetworkParameters params = bitcoiny.getNetworkParameters();
|
||||
String compoundKey = String.format("%s-%s-%d", params.getId(), p2shAddress, System.currentTimeMillis() / CACHE_TIMEOUT);
|
||||
|
||||
byte[] secret = SECRET_CACHE.getOrDefault(compoundKey, NO_SECRET_CACHE_ENTRY);
|
||||
if (secret != NO_SECRET_CACHE_ENTRY)
|
||||
return secret;
|
||||
|
||||
List<byte[]> rawTransactions = bitcoiny.getAddressTransactions(p2shAddress);
|
||||
|
||||
for (byte[] rawTransaction : rawTransactions) {
|
||||
Transaction transaction = new Transaction(params, rawTransaction);
|
||||
|
||||
// Cycle through inputs, looking for one that spends our HTLC
|
||||
for (TransactionInput input : transaction.getInputs()) {
|
||||
Script scriptSig = input.getScriptSig();
|
||||
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
|
||||
|
||||
// Expected number of script chunks for redeem. Refund might not have the same number.
|
||||
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
|
||||
if (scriptChunks.size() != expectedChunkCount)
|
||||
continue;
|
||||
|
||||
// We're expecting last chunk to contain the actual redeemScript
|
||||
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
|
||||
byte[] redeemScriptBytes = lastChunk.data;
|
||||
|
||||
// If non-push scripts, redeemScript will be null
|
||||
if (redeemScriptBytes == null)
|
||||
continue;
|
||||
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!inputAddress.toString().equals(p2shAddress))
|
||||
// Input isn't spending our HTLC
|
||||
continue;
|
||||
|
||||
secret = scriptChunks.get(0).data;
|
||||
if (secret.length != PirateChainHTLC.SECRET_LENGTH)
|
||||
continue;
|
||||
|
||||
// Cache secret for a while
|
||||
SECRET_CACHE.put(compoundKey, secret);
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache negative result
|
||||
SECRET_CACHE.put(compoundKey, null);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the txid of the transaction that funded supplied <tt>p2shAddress</tt>
|
||||
* We have to do this in a bit of a roundabout way due to the Pirate Light Client server omitting
|
||||
* transaction hashes from the raw transaction data.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
public static String getFundingTxid(BitcoinyBlockchainProvider blockchain, String p2shAddress) throws ForeignBlockchainException {
|
||||
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
|
||||
// HASH160(redeem script) for this p2shAddress
|
||||
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
|
||||
|
||||
|
||||
// Firstly look for an unspent output
|
||||
|
||||
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
|
||||
List<UnspentOutput> unspentOutputs = blockchain.getUnspentOutputs(p2shAddress, false);
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
|
||||
if (!Arrays.equals(ourScriptPubKey, unspentOutput.script)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return HashCode.fromBytes(unspentOutput.hash).toString();
|
||||
}
|
||||
|
||||
|
||||
// No valid unspent outputs, so must be already spent...
|
||||
|
||||
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
|
||||
List<BitcoinyTransaction> transactions = blockchain.getAddressBitcoinyTransactions(p2shAddress, BitcoinyBlockchainProvider.EXCLUDE_UNCONFIRMED);
|
||||
|
||||
// Sort by confirmed first, followed by ascending height
|
||||
transactions.sort(BitcoinyTransaction.CONFIRMED_FIRST.thenComparing(BitcoinyTransaction::getHeight));
|
||||
|
||||
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
|
||||
|
||||
// Acceptable funding is one transaction output, so we're expecting only one input
|
||||
if (bitcoinyTransaction.inputs.size() != 1)
|
||||
// Wrong number of inputs
|
||||
continue;
|
||||
|
||||
String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
|
||||
|
||||
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
|
||||
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
|
||||
// Not valid chunks for our form of HTLC
|
||||
continue;
|
||||
|
||||
// Last chunk is redeem script
|
||||
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
|
||||
// Not spending our specific HTLC redeem script
|
||||
continue;
|
||||
|
||||
return bitcoinyTransaction.inputs.get(0).outputTxHash;
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the unspent txid of the transaction that funded supplied <tt>p2shAddress</tt>
|
||||
* and is at least the value specified in <tt>minimumAmount</tt>
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
public static String getUnspentFundingTxid(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException {
|
||||
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
|
||||
|
||||
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
|
||||
List<UnspentOutput> unspentOutputs = blockchain.getUnspentOutputs(p2shAddress, false);
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
|
||||
if (!Arrays.equals(ourScriptPubKey, unspentOutput.script)) {
|
||||
// Not funding our specific HTLC script hash
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unspentOutput.value < minimumAmount) {
|
||||
// Not funding the required amount
|
||||
continue;
|
||||
}
|
||||
|
||||
return HashCode.fromBytes(unspentOutput.hash).toString();
|
||||
}
|
||||
|
||||
|
||||
// No valid unspent outputs, so must be already spent
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTLC status, given P2SH address and expected redeem/refund amount
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
public static Status determineHtlcStatus(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException {
|
||||
String compoundKey = String.format("%s-%s-%d", blockchain.getNetId(), p2shAddress, System.currentTimeMillis() / CACHE_TIMEOUT);
|
||||
|
||||
Status cachedStatus = STATUS_CACHE.getOrDefault(compoundKey, null);
|
||||
if (cachedStatus != null)
|
||||
return cachedStatus;
|
||||
|
||||
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
|
||||
|
||||
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
|
||||
List<BitcoinyTransaction> transactions = blockchain.getAddressBitcoinyTransactions(p2shAddress, BitcoinyBlockchainProvider.EXCLUDE_UNCONFIRMED);
|
||||
|
||||
// Sort by confirmed first, followed by ascending height
|
||||
transactions.sort(BitcoinyTransaction.CONFIRMED_FIRST.thenComparing(BitcoinyTransaction::getHeight));
|
||||
|
||||
// Transaction cache
|
||||
//Map<String, BitcoinyTransaction> transactionsByHash = new HashMap<>();
|
||||
// HASH160(redeem script) for this p2shAddress
|
||||
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
|
||||
|
||||
// Check for spends first, caching full transaction info as we progress just in case we don't return in this loop
|
||||
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
|
||||
|
||||
// Cache for possible later reuse
|
||||
// transactionsByHash.put(transactionInfo.txHash, bitcoinyTransaction);
|
||||
|
||||
// Acceptable funding is one transaction output, so we're expecting only one input
|
||||
if (bitcoinyTransaction.inputs.size() != 1)
|
||||
// Wrong number of inputs
|
||||
continue;
|
||||
|
||||
String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
|
||||
|
||||
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
|
||||
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
|
||||
// Not valid chunks for our form of HTLC
|
||||
continue;
|
||||
|
||||
// Last chunk is redeem script
|
||||
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
|
||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
|
||||
// Not spending our specific HTLC redeem script
|
||||
continue;
|
||||
|
||||
if (scriptSigChunks.size() == 4)
|
||||
// If we have 4 chunks, then secret is present, hence redeem
|
||||
cachedStatus = bitcoinyTransaction.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED;
|
||||
else
|
||||
cachedStatus = bitcoinyTransaction.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED;
|
||||
|
||||
STATUS_CACHE.put(compoundKey, cachedStatus);
|
||||
return cachedStatus;
|
||||
}
|
||||
|
||||
String ourScriptPubKeyHex = HashCode.fromBytes(ourScriptPubKey).toString();
|
||||
|
||||
// Check for funding
|
||||
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
|
||||
if (bitcoinyTransaction == null)
|
||||
// Should be present in map!
|
||||
throw new ForeignBlockchainException("Cached Bitcoin transaction now missing?");
|
||||
|
||||
// Check outputs for our specific P2SH
|
||||
for (BitcoinyTransaction.Output output : bitcoinyTransaction.outputs) {
|
||||
// Check amount
|
||||
if (output.value < minimumAmount)
|
||||
// Output amount too small (not taking fees into account)
|
||||
continue;
|
||||
|
||||
String scriptPubKeyHex = output.scriptPubKey;
|
||||
if (!scriptPubKeyHex.equals(ourScriptPubKeyHex))
|
||||
// Not funding our specific P2SH
|
||||
continue;
|
||||
|
||||
cachedStatus = bitcoinyTransaction.height == 0 ? Status.FUNDING_IN_PROGRESS : Status.FUNDED;
|
||||
STATUS_CACHE.put(compoundKey, cachedStatus);
|
||||
return cachedStatus;
|
||||
}
|
||||
}
|
||||
|
||||
cachedStatus = Status.UNFUNDED;
|
||||
STATUS_CACHE.put(compoundKey, cachedStatus);
|
||||
return cachedStatus;
|
||||
}
|
||||
|
||||
private static List<byte[]> extractScriptSigChunks(byte[] scriptSigBytes) {
|
||||
List<byte[]> chunks = new ArrayList<>();
|
||||
|
||||
int offset = 0;
|
||||
int previousOffset = 0;
|
||||
while (offset < scriptSigBytes.length) {
|
||||
byte pushOp = scriptSigBytes[offset++];
|
||||
|
||||
if (pushOp < 0 || pushOp > 0x4c)
|
||||
// Unacceptable OP
|
||||
return Collections.emptyList();
|
||||
|
||||
// Special treatment for OP_PUSHDATA1
|
||||
if (pushOp == 0x4c) {
|
||||
if (offset >= scriptSigBytes.length)
|
||||
// Run out of scriptSig bytes?
|
||||
return Collections.emptyList();
|
||||
|
||||
pushOp = scriptSigBytes[offset++];
|
||||
}
|
||||
|
||||
previousOffset = offset;
|
||||
offset += Byte.toUnsignedInt(pushOp);
|
||||
|
||||
byte[] chunk = Arrays.copyOfRange(scriptSigBytes, previousOffset, offset);
|
||||
chunks.add(chunk);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private static byte[] addressToScriptPubKey(String p2shAddress) {
|
||||
// We want the HASH160 part of the P2SH address
|
||||
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
|
||||
|
||||
byte[] scriptPubKey = new byte[1 + 1 + 20 + 1];
|
||||
scriptPubKey[0x00] = (byte) 0xa9; /* OP_HASH160 */
|
||||
scriptPubKey[0x01] = (byte) 0x14; /* PUSH 0x14 bytes */
|
||||
System.arraycopy(p2shAddressBytes, 1, scriptPubKey, 2, 0x14);
|
||||
scriptPubKey[0x16] = (byte) 0x87; /* OP_EQUAL */
|
||||
|
||||
return scriptPubKey;
|
||||
}
|
||||
|
||||
private static byte[] addressToRedeemScriptHash(String p2shAddress) {
|
||||
// We want the HASH160 part of the P2SH address
|
||||
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
|
||||
|
||||
return Arrays.copyOfRange(p2shAddressBytes, 1, 1 + 20);
|
||||
}
|
||||
|
||||
}
|
||||
649
src/main/java/org/qortal/crosschain/PirateLightClient.java
Normal file
649
src/main/java/org/qortal/crosschain/PirateLightClient.java
Normal file
@@ -0,0 +1,649 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc;
|
||||
import cash.z.wallet.sdk.rpc.Service;
|
||||
import cash.z.wallet.sdk.rpc.Service.*;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Pirate Chain network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
|
||||
public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(PirateLightClient.class);
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private static final int RESPONSE_TIME_READINGS = 5;
|
||||
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
|
||||
|
||||
public static class Server {
|
||||
String hostname;
|
||||
|
||||
public enum ConnectionType { TCP, SSL }
|
||||
ConnectionType connectionType;
|
||||
|
||||
int port;
|
||||
private List<Long> responseTimes = new ArrayList<>();
|
||||
|
||||
public Server(String hostname, ConnectionType connectionType, int port) {
|
||||
this.hostname = hostname;
|
||||
this.connectionType = connectionType;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public void addResponseTime(long responseTime) {
|
||||
while (this.responseTimes.size() > RESPONSE_TIME_READINGS) {
|
||||
this.responseTimes.remove(0);
|
||||
}
|
||||
this.responseTimes.add(responseTime);
|
||||
}
|
||||
|
||||
public long averageResponseTime() {
|
||||
if (this.responseTimes.size() < RESPONSE_TIME_READINGS) {
|
||||
// Not enough readings yet
|
||||
return 0L;
|
||||
}
|
||||
OptionalDouble average = this.responseTimes.stream().mapToDouble(a -> a).average();
|
||||
if (average.isPresent()) {
|
||||
return Double.valueOf(average.getAsDouble()).longValue();
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == this)
|
||||
return true;
|
||||
|
||||
if (!(other instanceof Server))
|
||||
return false;
|
||||
|
||||
Server otherServer = (Server) other;
|
||||
|
||||
return this.connectionType == otherServer.connectionType
|
||||
&& this.port == otherServer.port
|
||||
&& this.hostname.equals(otherServer.hostname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.hostname.hashCode() ^ this.port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
|
||||
}
|
||||
}
|
||||
private Set<Server> servers = new HashSet<>();
|
||||
private List<Server> remainingServers = new ArrayList<>();
|
||||
private Set<Server> uselessServers = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private final String netId;
|
||||
private final String expectedGenesisHash;
|
||||
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
|
||||
private Bitcoiny blockchain;
|
||||
|
||||
private final Object serverLock = new Object();
|
||||
private Server currentServer;
|
||||
private ManagedChannel channel;
|
||||
private int nextId = 1;
|
||||
|
||||
private static final int TX_CACHE_SIZE = 1000;
|
||||
@SuppressWarnings("serial")
|
||||
private final Map<String, BitcoinyTransaction> transactionCache = Collections.synchronizedMap(new LinkedHashMap<>(TX_CACHE_SIZE + 1, 0.75F, true) {
|
||||
// This method is called just after a new entry has been added
|
||||
@Override
|
||||
public boolean removeEldestEntry(Map.Entry<String, BitcoinyTransaction> eldest) {
|
||||
return size() > TX_CACHE_SIZE;
|
||||
}
|
||||
});
|
||||
|
||||
// Constructors
|
||||
|
||||
public PirateLightClient(String netId, String genesisHash, Collection<Server> initialServerList, Map<Server.ConnectionType, Integer> defaultPorts) {
|
||||
this.netId = netId;
|
||||
this.expectedGenesisHash = genesisHash;
|
||||
this.servers.addAll(initialServerList);
|
||||
this.defaultPorts.putAll(defaultPorts);
|
||||
}
|
||||
|
||||
// Methods for use by other classes
|
||||
|
||||
@Override
|
||||
public void setBlockchain(Bitcoiny blockchain) {
|
||||
this.blockchain = blockchain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNetId() {
|
||||
return this.netId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current blockchain height.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public int getCurrentHeight() throws ForeignBlockchainException {
|
||||
BlockID latestBlock = this.getCompactTxStreamerStub().getLatestBlock(null);
|
||||
|
||||
if (!(latestBlock instanceof BlockID))
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getLatestBlock gRPC");
|
||||
|
||||
return (int)latestBlock.getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of compact blocks, starting from <tt>startHeight</tt> inclusive.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
|
||||
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
|
||||
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
|
||||
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||
|
||||
Iterator<CompactBlock> blocksIterator = this.getCompactTxStreamerStub().getBlockRange(range);
|
||||
|
||||
// Map from Iterator to List
|
||||
List<CompactBlock> blocks = new ArrayList<>();
|
||||
blocksIterator.forEachRemaining(blocks::add);
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException {
|
||||
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
|
||||
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
|
||||
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||
|
||||
Iterator<CompactBlock> blocks = this.getCompactTxStreamerStub().getBlockRange(range);
|
||||
|
||||
List<byte[]> rawBlockHeaders = new ArrayList<>();
|
||||
|
||||
while (blocks.hasNext()) {
|
||||
CompactBlock block = blocks.next();
|
||||
|
||||
if (block.getHeader() == null) {
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getBlockRange gRPC");
|
||||
}
|
||||
|
||||
rawBlockHeaders.add(block.getHeader().toByteArray());
|
||||
}
|
||||
|
||||
return rawBlockHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of raw block timestamps, starting from <tt>startHeight</tt> inclusive.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException {
|
||||
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
|
||||
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
|
||||
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||
|
||||
Iterator<CompactBlock> blocks = this.getCompactTxStreamerStub().getBlockRange(range);
|
||||
|
||||
List<Long> rawBlockTimestamps = new ArrayList<>();
|
||||
|
||||
while (blocks.hasNext()) {
|
||||
CompactBlock block = blocks.next();
|
||||
|
||||
if (block.getTime() <= 0) {
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getBlockRange gRPC");
|
||||
}
|
||||
|
||||
rawBlockTimestamps.add(Long.valueOf(block.getTime()));
|
||||
}
|
||||
|
||||
return rawBlockTimestamps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns confirmed balance, based on passed payment script.
|
||||
* <p>
|
||||
* @return confirmed balance, or zero if script unknown
|
||||
* @throws ForeignBlockchainException if there was an error
|
||||
*/
|
||||
@Override
|
||||
public long getConfirmedBalance(byte[] script) throws ForeignBlockchainException {
|
||||
throw new ForeignBlockchainException("getConfirmedBalance not yet implemented for Pirate Chain");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns confirmed balance, based on passed base58 encoded address.
|
||||
* <p>
|
||||
* @return confirmed balance, or zero if address unknown
|
||||
* @throws ForeignBlockchainException if there was an error
|
||||
*/
|
||||
@Override
|
||||
public long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException {
|
||||
AddressList addressList = AddressList.newBuilder().addAddresses(base58Address).build();
|
||||
Balance balance = this.getCompactTxStreamerStub().getTaddressBalance(addressList);
|
||||
|
||||
if (!(balance instanceof Balance))
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getConfirmedAddressBalance gRPC");
|
||||
|
||||
return balance.getValueZat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of unspent outputs pertaining to passed address.
|
||||
* <p>
|
||||
* @return list of unspent outputs, or empty list if address unknown
|
||||
* @throws ForeignBlockchainException if there was an error.
|
||||
*/
|
||||
@Override
|
||||
public List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
GetAddressUtxosArg getAddressUtxosArg = GetAddressUtxosArg.newBuilder().addAddresses(address).build();
|
||||
GetAddressUtxosReplyList replyList = this.getCompactTxStreamerStub().getAddressUtxos(getAddressUtxosArg);
|
||||
|
||||
if (!(replyList instanceof GetAddressUtxosReplyList))
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getUnspentOutputs gRPC");
|
||||
|
||||
List<GetAddressUtxosReply> unspentList = replyList.getAddressUtxosList();
|
||||
if (unspentList == null)
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getUnspentOutputs gRPC");
|
||||
|
||||
List<UnspentOutput> unspentOutputs = new ArrayList<>();
|
||||
for (GetAddressUtxosReply unspent : unspentList) {
|
||||
|
||||
int height = (int)unspent.getHeight();
|
||||
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
|
||||
if (!includeUnconfirmed && height <= 0)
|
||||
continue;
|
||||
|
||||
byte[] txHash = unspent.getTxid().toByteArray();
|
||||
int outputIndex = unspent.getIndex();
|
||||
long value = unspent.getValueZat();
|
||||
byte[] script = unspent.getScript().toByteArray();
|
||||
String addressRes = unspent.getAddress();
|
||||
|
||||
unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value, script, addressRes));
|
||||
}
|
||||
|
||||
return unspentOutputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of unspent outputs pertaining to passed payment script.
|
||||
* <p>
|
||||
* @return list of unspent outputs, or empty list if script unknown
|
||||
* @throws ForeignBlockchainException if there was an error.
|
||||
*/
|
||||
@Override
|
||||
public List<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
String address = this.blockchain.deriveP2shAddress(script);
|
||||
return this.getUnspentOutputs(address, includeUnconfirmed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns raw transaction for passed transaction hash.
|
||||
* <p>
|
||||
* NOTE: Do not mutate returned byte[]!
|
||||
*
|
||||
* @throws ForeignBlockchainException.NotFoundException if transaction not found
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public byte[] getRawTransaction(String txHash) throws ForeignBlockchainException {
|
||||
return getRawTransaction(HashCode.fromString(txHash).asBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns raw transaction for passed transaction hash.
|
||||
* <p>
|
||||
* NOTE: Do not mutate returned byte[]!
|
||||
*
|
||||
* @throws ForeignBlockchainException.NotFoundException if transaction not found
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException {
|
||||
ByteString byteString = ByteString.copyFrom(txHash);
|
||||
TxFilter txFilter = TxFilter.newBuilder().setHash(byteString).build();
|
||||
RawTransaction rawTransaction = this.getCompactTxStreamerStub().getTransaction(txFilter);
|
||||
|
||||
if (!(rawTransaction instanceof RawTransaction))
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getTransaction gRPC");
|
||||
|
||||
return rawTransaction.getData().toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns transaction info for passed transaction hash.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException.NotFoundException if transaction not found
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
|
||||
// Check cache first
|
||||
BitcoinyTransaction transaction = transactionCache.get(txHash);
|
||||
if (transaction != null)
|
||||
return transaction;
|
||||
|
||||
ByteString byteString = ByteString.copyFrom(HashCode.fromString(txHash).asBytes());
|
||||
TxFilter txFilter = TxFilter.newBuilder().setHash(byteString).build();
|
||||
RawTransaction rawTransaction = this.getCompactTxStreamerStub().getTransaction(txFilter);
|
||||
|
||||
if (!(rawTransaction instanceof RawTransaction))
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getTransaction gRPC");
|
||||
|
||||
byte[] transactionData = rawTransaction.getData().toByteArray();
|
||||
String transactionDataString = HashCode.fromBytes(transactionData).toString();
|
||||
|
||||
JSONParser parser = new JSONParser();
|
||||
JSONObject transactionJson;
|
||||
try {
|
||||
transactionJson = (JSONObject) parser.parse(transactionDataString);
|
||||
} catch (ParseException e) {
|
||||
throw new ForeignBlockchainException.NetworkException("Expected JSON string from Pirate Chain getTransaction gRPC");
|
||||
}
|
||||
|
||||
Object inputsObj = transactionJson.get("vin");
|
||||
if (!(inputsObj instanceof JSONArray))
|
||||
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vin' from Pirate Chain getTransaction gRPC");
|
||||
|
||||
Object outputsObj = transactionJson.get("vout");
|
||||
if (!(outputsObj instanceof JSONArray))
|
||||
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vout' from Pirate Chain getTransaction gRPC");
|
||||
|
||||
try {
|
||||
int size = ((Long) transactionJson.get("size")).intValue();
|
||||
int locktime = ((Long) transactionJson.get("locktime")).intValue();
|
||||
|
||||
// Timestamp might not be present, e.g. for unconfirmed transaction
|
||||
Object timeObj = transactionJson.get("time");
|
||||
Integer timestamp = timeObj != null
|
||||
? ((Long) timeObj).intValue()
|
||||
: null;
|
||||
|
||||
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
|
||||
for (Object inputObj : (JSONArray) inputsObj) {
|
||||
JSONObject inputJson = (JSONObject) inputObj;
|
||||
|
||||
String scriptSig = (String) ((JSONObject) inputJson.get("scriptSig")).get("hex");
|
||||
int sequence = ((Long) inputJson.get("sequence")).intValue();
|
||||
String outputTxHash = (String) inputJson.get("txid");
|
||||
int outputVout = ((Long) inputJson.get("vout")).intValue();
|
||||
|
||||
inputs.add(new BitcoinyTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
|
||||
}
|
||||
|
||||
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
|
||||
for (Object outputObj : (JSONArray) outputsObj) {
|
||||
JSONObject outputJson = (JSONObject) outputObj;
|
||||
|
||||
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
|
||||
long value = BigDecimal.valueOf((Double) outputJson.get("value")).setScale(8).unscaledValue().longValue();
|
||||
|
||||
// address too, if present in the "addresses" array
|
||||
List<String> addresses = null;
|
||||
Object addressesObj = ((JSONObject) outputJson.get("scriptPubKey")).get("addresses");
|
||||
if (addressesObj instanceof JSONArray) {
|
||||
addresses = new ArrayList<>();
|
||||
for (Object addressObj : (JSONArray) addressesObj) {
|
||||
addresses.add((String) addressObj);
|
||||
}
|
||||
}
|
||||
|
||||
// some peers return a single "address" string
|
||||
Object addressObj = ((JSONObject) outputJson.get("scriptPubKey")).get("address");
|
||||
if (addressObj instanceof String) {
|
||||
if (addresses == null) {
|
||||
addresses = new ArrayList<>();
|
||||
}
|
||||
addresses.add((String) addressObj);
|
||||
}
|
||||
|
||||
// For the purposes of Qortal we require all outputs to contain addresses
|
||||
// Some servers omit this info, causing problems down the line with balance calculations
|
||||
// Update: it turns out that they were just using a different key - "address" instead of "addresses"
|
||||
// The code below can remain in place, just in case a peer returns a missing address in the future
|
||||
if (addresses == null || addresses.isEmpty()) {
|
||||
if (this.currentServer != null) {
|
||||
this.uselessServers.add(this.currentServer);
|
||||
this.closeServer(this.currentServer);
|
||||
}
|
||||
LOGGER.info("No output addresses returned for transaction {}", txHash);
|
||||
throw new ForeignBlockchainException(String.format("No output addresses returned for transaction %s", txHash));
|
||||
}
|
||||
|
||||
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses));
|
||||
}
|
||||
|
||||
transaction = new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
|
||||
|
||||
// Save into cache
|
||||
transactionCache.put(txHash, transaction);
|
||||
|
||||
return transaction;
|
||||
} catch (NullPointerException | ClassCastException e) {
|
||||
// Unexpected / invalid response from ElectrumX server
|
||||
}
|
||||
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected JSON format from Pirate Chain getTransaction gRPC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of transactions, relating to passed payment script.
|
||||
* <p>
|
||||
* @return list of related transactions, or empty list if script unknown
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
// FUTURE: implement this if needed. Probably not very useful for private blockchains.
|
||||
throw new ForeignBlockchainException("getAddressTransactions not yet implemented for Pirate Chain");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
try {
|
||||
// Firstly we need to get the latest block
|
||||
int defaultBirthday = Settings.getInstance().getArrrDefaultBirthday();
|
||||
BlockID endBlock = this.getCompactTxStreamerStub().getLatestBlock(null);
|
||||
BlockID startBlock = BlockID.newBuilder().setHeight(defaultBirthday).build();
|
||||
BlockRange blockRange = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||
|
||||
TransparentAddressBlockFilter blockFilter = TransparentAddressBlockFilter.newBuilder()
|
||||
.setAddress(address)
|
||||
.setRange(blockRange)
|
||||
.build();
|
||||
Iterator<Service.RawTransaction> transactionIterator = this.getCompactTxStreamerStub().getTaddressTxids(blockFilter);
|
||||
|
||||
// Map from Iterator to List
|
||||
List<RawTransaction> rawTransactions = new ArrayList<>();
|
||||
transactionIterator.forEachRemaining(rawTransactions::add);
|
||||
|
||||
List<BitcoinyTransaction> transactions = new ArrayList<>();
|
||||
|
||||
for (RawTransaction rawTransaction : rawTransactions) {
|
||||
|
||||
Long height = rawTransaction.getHeight();
|
||||
if (!includeUnconfirmed && (height == null || height == 0))
|
||||
// We only want confirmed transactions
|
||||
continue;
|
||||
|
||||
byte[] transactionData = rawTransaction.getData().toByteArray();
|
||||
String transactionDataHex = HashCode.fromBytes(transactionData).toString();
|
||||
BitcoinyTransaction bitcoinyTransaction = PirateChain.deserializeRawTransaction(transactionDataHex);
|
||||
bitcoinyTransaction.height = height.intValue();
|
||||
transactions.add(bitcoinyTransaction);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
catch (RuntimeException | TransformationException e) {
|
||||
throw new ForeignBlockchainException(String.format("Unable to get transactions for address %s: %s", address, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts raw transaction to network.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
@Override
|
||||
public void broadcastTransaction(byte[] transactionBytes) throws ForeignBlockchainException {
|
||||
ByteString byteString = ByteString.copyFrom(transactionBytes);
|
||||
RawTransaction rawTransaction = RawTransaction.newBuilder().setData(byteString).build();
|
||||
SendResponse sendResponse = this.getCompactTxStreamerStub().sendTransaction(rawTransaction);
|
||||
|
||||
if (!(sendResponse instanceof SendResponse))
|
||||
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain broadcastTransaction gRPC");
|
||||
|
||||
if (sendResponse.getErrorCode() != 0)
|
||||
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error code from Pirate Chain broadcastTransaction gRPC: %d", sendResponse.getErrorCode()));
|
||||
}
|
||||
|
||||
// Class-private utility methods
|
||||
|
||||
|
||||
/**
|
||||
* Performs RPC call, with automatic reconnection to different server if needed.
|
||||
* <p>
|
||||
* @return "result" object from within JSON output
|
||||
* @throws ForeignBlockchainException if server returns error or something goes wrong
|
||||
*/
|
||||
private CompactTxStreamerGrpc.CompactTxStreamerBlockingStub getCompactTxStreamerStub() throws ForeignBlockchainException {
|
||||
synchronized (this.serverLock) {
|
||||
if (this.remainingServers.isEmpty())
|
||||
this.remainingServers.addAll(this.servers);
|
||||
|
||||
while (haveConnection()) {
|
||||
// If we have more servers and the last one replied slowly, try another
|
||||
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);
|
||||
this.closeServer();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return CompactTxStreamerGrpc.newBlockingStub(this.channel);
|
||||
|
||||
// // Didn't work, try another server...
|
||||
// this.closeServer();
|
||||
}
|
||||
|
||||
// Failed to perform RPC - maybe lack of servers?
|
||||
LOGGER.info("Error: No connected Pirate Light servers when trying to make RPC call");
|
||||
throw new ForeignBlockchainException.NetworkException("No connected Pirate Light servers when trying to make RPC call");
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if we have, or create, a connection to an ElectrumX server. */
|
||||
private boolean haveConnection() throws ForeignBlockchainException {
|
||||
if (this.currentServer != null && this.channel != null && !this.channel.isShutdown())
|
||||
return true;
|
||||
|
||||
while (!this.remainingServers.isEmpty()) {
|
||||
Server 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();
|
||||
|
||||
CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel);
|
||||
LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build());
|
||||
|
||||
if (lightdInfo == null || lightdInfo.getBlockHeight() <= 0)
|
||||
continue;
|
||||
|
||||
// TODO: find a way to verify that the server is using the expected chain
|
||||
|
||||
// if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
|
||||
// continue;
|
||||
|
||||
// if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
|
||||
// continue;
|
||||
|
||||
LOGGER.debug(() -> String.format("Connected to %s", server));
|
||||
this.currentServer = server;
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
// Didn't work, try another server...
|
||||
closeServer();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes connection to <tt>server</tt> if it is currently connected server.
|
||||
* @param server
|
||||
*/
|
||||
private void closeServer(Server server) {
|
||||
synchronized (this.serverLock) {
|
||||
if (this.currentServer == null || !this.currentServer.equals(server) || this.channel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the gRPC managed-channel if not shut down already.
|
||||
if (!this.channel.isShutdown()) {
|
||||
try {
|
||||
this.channel.shutdown();
|
||||
if (!this.channel.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
LOGGER.warn("Timed out gracefully shutting down connection: {}. ", this.channel);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Unexpected exception while waiting for channel termination", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Forceful shut down if still not terminated.
|
||||
if (!this.channel.isTerminated()) {
|
||||
try {
|
||||
this.channel.shutdownNow();
|
||||
if (!this.channel.awaitTermination(15, TimeUnit.SECONDS)) {
|
||||
LOGGER.warn("Timed out forcefully shutting down connection: {}. ", this.channel);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Unexpected exception while waiting for channel termination", e);
|
||||
}
|
||||
}
|
||||
|
||||
this.channel = null;
|
||||
this.currentServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes connection to currently connected server (if any). */
|
||||
private void closeServer() {
|
||||
synchronized (this.serverLock) {
|
||||
this.closeServer(this.currentServer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
409
src/main/java/org/qortal/crosschain/PirateWallet.java
Normal file
409
src/main/java/org/qortal/crosschain/PirateWallet.java
Normal file
@@ -0,0 +1,409 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import com.rust.litewalletjni.LiteWalletJni;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.bouncycastle.util.encoders.DecoderException;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.controller.PirateChainWalletController;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
public class PirateWallet {
|
||||
|
||||
protected static final Logger LOGGER = LogManager.getLogger(PirateWallet.class);
|
||||
|
||||
private byte[] entropyBytes;
|
||||
private final boolean isNullSeedWallet;
|
||||
private String seedPhrase;
|
||||
private boolean ready = false;
|
||||
|
||||
private String params;
|
||||
private String saplingOutput64;
|
||||
private String saplingSpend64;
|
||||
|
||||
private final static String COIN_PARAMS_FILENAME = "coinparams.json";
|
||||
private final static String SAPLING_OUTPUT_FILENAME = "saplingoutput_base64";
|
||||
private final static String SAPLING_SPEND_FILENAME = "saplingspend_base64";
|
||||
|
||||
public PirateWallet(byte[] entropyBytes, boolean isNullSeedWallet) throws IOException {
|
||||
this.entropyBytes = entropyBytes;
|
||||
this.isNullSeedWallet = isNullSeedWallet;
|
||||
|
||||
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
|
||||
if (!Files.exists(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.params = Files.readString(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME));
|
||||
this.saplingOutput64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_OUTPUT_FILENAME));
|
||||
this.saplingSpend64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_SPEND_FILENAME));
|
||||
|
||||
this.ready = this.initialize();
|
||||
}
|
||||
|
||||
private boolean initialize() {
|
||||
try {
|
||||
LiteWalletJni.initlogging();
|
||||
|
||||
if (this.entropyBytes == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pick a random server
|
||||
PirateLightClient.Server server = this.getRandomServer();
|
||||
String serverUri = String.format("https://%s:%d/", server.hostname, server.port);
|
||||
|
||||
// Pirate library uses base64 encoding
|
||||
String entropy64 = Base64.toBase64String(this.entropyBytes);
|
||||
|
||||
// Derive seed phrase from entropy bytes
|
||||
String inputSeedResponse = LiteWalletJni.getseedphrasefromentropyb64(entropy64);
|
||||
JSONObject inputSeedJson = new JSONObject(inputSeedResponse);
|
||||
String inputSeedPhrase = null;
|
||||
if (inputSeedJson.has("seedPhrase")) {
|
||||
inputSeedPhrase = inputSeedJson.getString("seedPhrase");
|
||||
}
|
||||
|
||||
String wallet = this.load();
|
||||
if (wallet == null) {
|
||||
// Wallet doesn't exist, so create a new one
|
||||
|
||||
int birthday = Settings.getInstance().getArrrDefaultBirthday();
|
||||
if (this.isNullSeedWallet) {
|
||||
try {
|
||||
// Attempt to set birthday to the current block for null seed wallets
|
||||
birthday = PirateChain.getInstance().blockchainProvider.getCurrentHeight();
|
||||
}
|
||||
catch (ForeignBlockchainException e) {
|
||||
// Use the default height
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize new wallet
|
||||
String birthdayString = String.format("%d", birthday);
|
||||
String outputSeedResponse = LiteWalletJni.initfromseed(serverUri, this.params, inputSeedPhrase, birthdayString, this.saplingOutput64, this.saplingSpend64); // Thread-safe.
|
||||
JSONObject outputSeedJson = new JSONObject(outputSeedResponse);
|
||||
String outputSeedPhrase = null;
|
||||
if (outputSeedJson.has("seed")) {
|
||||
outputSeedPhrase = outputSeedJson.getString("seed");
|
||||
}
|
||||
|
||||
// Ensure seed phrase in response matches supplied seed phrase
|
||||
if (inputSeedPhrase == null || !Objects.equals(inputSeedPhrase, outputSeedPhrase)) {
|
||||
LOGGER.info("Unable to initialize Pirate Chain wallet: seed phrases do not match, or are null");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.seedPhrase = outputSeedPhrase;
|
||||
|
||||
} else {
|
||||
// Restore existing wallet
|
||||
String response = LiteWalletJni.initfromb64(serverUri, params, wallet, saplingOutput64, saplingSpend64);
|
||||
if (response != null && !response.contains("\"initalized\":true")) {
|
||||
LOGGER.info("Unable to initialize Pirate Chain wallet: {}", response);
|
||||
return false;
|
||||
}
|
||||
this.seedPhrase = inputSeedPhrase;
|
||||
}
|
||||
|
||||
// Check that we're able to communicate with the library
|
||||
Integer ourHeight = this.getHeight();
|
||||
return (ourHeight != null && ourHeight > 0);
|
||||
|
||||
} catch (IOException | JSONException | UnsatisfiedLinkError e) {
|
||||
LOGGER.info("Unable to initialize Pirate Chain wallet: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return this.ready;
|
||||
}
|
||||
|
||||
public void setReady(boolean ready) {
|
||||
this.ready = ready;
|
||||
}
|
||||
|
||||
public boolean entropyBytesEqual(byte[] testEntropyBytes) {
|
||||
return Arrays.equals(testEntropyBytes, this.entropyBytes);
|
||||
}
|
||||
|
||||
private void encrypt() {
|
||||
if (this.isEncrypted()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
String encryptionKey = this.getEncryptionKey();
|
||||
if (encryptionKey == null) {
|
||||
// Can't encrypt without a key
|
||||
return;
|
||||
}
|
||||
|
||||
this.doEncrypt(encryptionKey);
|
||||
}
|
||||
|
||||
private void decrypt() {
|
||||
if (!this.isEncrypted()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
String encryptionKey = this.getEncryptionKey();
|
||||
if (encryptionKey == null) {
|
||||
// Can't encrypt without a key
|
||||
return;
|
||||
}
|
||||
|
||||
this.doDecrypt(encryptionKey);
|
||||
}
|
||||
|
||||
public void unlock() {
|
||||
if (!this.isEncrypted()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
String encryptionKey = this.getEncryptionKey();
|
||||
if (encryptionKey == null) {
|
||||
// Can't encrypt without a key
|
||||
return;
|
||||
}
|
||||
|
||||
this.doUnlock(encryptionKey);
|
||||
}
|
||||
|
||||
public boolean save() throws IOException {
|
||||
if (!isInitialized()) {
|
||||
LOGGER.info("Error: can't save wallet, because no wallet it initialized");
|
||||
return false;
|
||||
}
|
||||
if (this.isNullSeedWallet()) {
|
||||
// Don't save wallets that have a null seed
|
||||
return false;
|
||||
}
|
||||
|
||||
// Encrypt first (will do nothing if already encrypted)
|
||||
this.encrypt();
|
||||
|
||||
String wallet64 = LiteWalletJni.save();
|
||||
byte[] wallet;
|
||||
try {
|
||||
wallet = Base64.decode(wallet64);
|
||||
}
|
||||
catch (DecoderException e) {
|
||||
LOGGER.info("Unable to decode wallet");
|
||||
return false;
|
||||
}
|
||||
if (wallet == null) {
|
||||
LOGGER.info("Unable to save wallet");
|
||||
return false;
|
||||
}
|
||||
|
||||
Path walletPath = this.getCurrentWalletPath();
|
||||
Files.createDirectories(walletPath.getParent());
|
||||
Files.write(walletPath, wallet, StandardOpenOption.CREATE);
|
||||
|
||||
LOGGER.debug("Saved Pirate Chain wallet");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public String load() throws IOException {
|
||||
if (this.isNullSeedWallet()) {
|
||||
// Don't load wallets that have a null seed
|
||||
return null;
|
||||
}
|
||||
Path walletPath = this.getCurrentWalletPath();
|
||||
if (!Files.exists(walletPath)) {
|
||||
return null;
|
||||
}
|
||||
byte[] wallet = Files.readAllBytes(walletPath);
|
||||
if (wallet == null) {
|
||||
return null;
|
||||
}
|
||||
String wallet64 = Base64.toBase64String(wallet);
|
||||
return wallet64;
|
||||
}
|
||||
|
||||
private String getEntropyHash58() {
|
||||
if (this.entropyBytes == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] entropyHash = Crypto.digest(this.entropyBytes);
|
||||
return Base58.encode(entropyHash);
|
||||
}
|
||||
|
||||
public String getSeedPhrase() {
|
||||
return this.seedPhrase;
|
||||
}
|
||||
|
||||
private String getEncryptionKey() {
|
||||
if (this.entropyBytes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prefix the bytes with a (deterministic) string, to ensure that the resulting hash is different
|
||||
String prefix = "ARRRWalletEncryption";
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try {
|
||||
outputStream.write(prefix.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.write(this.entropyBytes);
|
||||
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] encryptionKeyHash = Crypto.digest(outputStream.toByteArray());
|
||||
return Base58.encode(encryptionKeyHash);
|
||||
}
|
||||
|
||||
private Path getCurrentWalletPath() {
|
||||
String entropyHash58 = this.getEntropyHash58();
|
||||
String filename = String.format("wallet-%s.dat", entropyHash58);
|
||||
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", filename);
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return this.entropyBytes != null && this.ready;
|
||||
}
|
||||
|
||||
public boolean isSynchronized() {
|
||||
Integer height = this.getHeight();
|
||||
Integer chainTip = this.getChainTip();
|
||||
|
||||
if (height == null || chainTip == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assume synchronized if within 2 blocks of the chain tip
|
||||
return height >= (chainTip - 2);
|
||||
}
|
||||
|
||||
|
||||
// APIs
|
||||
|
||||
public Integer getHeight() {
|
||||
String response = LiteWalletJni.execute("height", "");
|
||||
JSONObject json = new JSONObject(response);
|
||||
if (json.has("height")) {
|
||||
return json.getInt("height");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Integer getChainTip() {
|
||||
String response = LiteWalletJni.execute("info", "");
|
||||
JSONObject json = new JSONObject(response);
|
||||
if (json.has("latest_block_height")) {
|
||||
return json.getInt("latest_block_height");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isNullSeedWallet() {
|
||||
return this.isNullSeedWallet;
|
||||
}
|
||||
|
||||
public Boolean isEncrypted() {
|
||||
String response = LiteWalletJni.execute("encryptionstatus", "");
|
||||
JSONObject json = new JSONObject(response);
|
||||
if (json.has("encrypted")) {
|
||||
return json.getBoolean("encrypted");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean doEncrypt(String key) {
|
||||
String response = LiteWalletJni.execute("encrypt", key);
|
||||
JSONObject json = new JSONObject(response);
|
||||
String result = json.getString("result");
|
||||
if (json.has("result")) {
|
||||
return (Objects.equals(result, "success"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean doDecrypt(String key) {
|
||||
String response = LiteWalletJni.execute("decrypt", key);
|
||||
JSONObject json = new JSONObject(response);
|
||||
String result = json.getString("result");
|
||||
if (json.has("result")) {
|
||||
return (Objects.equals(result, "success"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean doUnlock(String key) {
|
||||
String response = LiteWalletJni.execute("unlock", key);
|
||||
JSONObject json = new JSONObject(response);
|
||||
String result = json.getString("result");
|
||||
if (json.has("result")) {
|
||||
return (Objects.equals(result, "success"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getWalletAddress() {
|
||||
// Get balance, which also contains wallet addresses
|
||||
String response = LiteWalletJni.execute("balance", "");
|
||||
JSONObject json = new JSONObject(response);
|
||||
String address = null;
|
||||
|
||||
if (json.has("z_addresses")) {
|
||||
JSONArray z_addresses = json.getJSONArray("z_addresses");
|
||||
|
||||
if (z_addresses != null && !z_addresses.isEmpty()) {
|
||||
JSONObject firstAddress = z_addresses.getJSONObject(0);
|
||||
if (firstAddress.has("address")) {
|
||||
address = firstAddress.getString("address");
|
||||
}
|
||||
}
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
public String getPrivateKey() {
|
||||
String response = LiteWalletJni.execute("export", "");
|
||||
JSONArray addressesJson = new JSONArray(response);
|
||||
if (!addressesJson.isEmpty()) {
|
||||
JSONObject addressJson = addressesJson.getJSONObject(0);
|
||||
if (addressJson.has("private_key")) {
|
||||
//String address = addressJson.getString("address");
|
||||
String privateKey = addressJson.getString("private_key");
|
||||
//String viewingKey = addressJson.getString("viewing_key");
|
||||
|
||||
return privateKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public PirateLightClient.Server getRandomServer() {
|
||||
PirateChain.PirateChainNet pirateChainNet = Settings.getInstance().getPirateChainNet();
|
||||
Collection<PirateLightClient.Server> servers = pirateChainNet.getServers();
|
||||
Random random = new Random();
|
||||
int index = random.nextInt(servers.size());
|
||||
return (PirateLightClient.Server) servers.toArray()[index];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,11 +7,12 @@ import java.util.List;
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class SimpleTransaction {
|
||||
private String txHash;
|
||||
private Integer timestamp;
|
||||
private Long timestamp;
|
||||
private long totalAmount;
|
||||
private long feeAmount;
|
||||
private List<Input> inputs;
|
||||
private List<Output> outputs;
|
||||
private String memo;
|
||||
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@@ -74,20 +75,21 @@ public class SimpleTransaction {
|
||||
public SimpleTransaction() {
|
||||
}
|
||||
|
||||
public SimpleTransaction(String txHash, Integer timestamp, long totalAmount, long feeAmount, List<Input> inputs, List<Output> outputs) {
|
||||
public SimpleTransaction(String txHash, Long timestamp, long totalAmount, long feeAmount, List<Input> inputs, List<Output> outputs, String memo) {
|
||||
this.txHash = txHash;
|
||||
this.timestamp = timestamp;
|
||||
this.totalAmount = totalAmount;
|
||||
this.feeAmount = feeAmount;
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
this.memo = memo;
|
||||
}
|
||||
|
||||
public String getTxHash() {
|
||||
return txHash;
|
||||
}
|
||||
|
||||
public Integer getTimestamp() {
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,20 @@ public enum SupportedBlockchain {
|
||||
public ACCT getLatestAcct() {
|
||||
return RavencoinACCTv3.getInstance();
|
||||
}
|
||||
},
|
||||
|
||||
PIRATECHAIN(Arrays.asList(
|
||||
Triple.valueOf(PirateChainACCTv3.NAME, PirateChainACCTv3.CODE_BYTES_HASH, PirateChainACCTv3::getInstance)
|
||||
)) {
|
||||
@Override
|
||||
public ForeignBlockchain getInstance() {
|
||||
return PirateChain.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ACCT getLatestAcct() {
|
||||
return PirateChainACCTv3.getInstance();
|
||||
}
|
||||
};
|
||||
|
||||
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
|
||||
|
||||
@@ -7,10 +7,20 @@ public class UnspentOutput {
|
||||
public final int height;
|
||||
public final long value;
|
||||
|
||||
public UnspentOutput(byte[] hash, int index, int height, long value) {
|
||||
// Optional fields returned by Pirate Light Client server
|
||||
public final byte[] script;
|
||||
public final String address;
|
||||
|
||||
public UnspentOutput(byte[] hash, int index, int height, long value, byte[] script, String address) {
|
||||
this.hash = hash;
|
||||
this.index = index;
|
||||
this.height = height;
|
||||
this.value = value;
|
||||
this.script = script;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public UnspentOutput(byte[] hash, int index, int height, long value) {
|
||||
this(hash, index, height, value, null, null);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package org.qortal.crypto;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.math.ec.rfc7748.X25519;
|
||||
import org.bouncycastle.math.ec.rfc7748.X25519Field;
|
||||
import org.bouncycastle.math.ec.rfc8032.Ed25519;
|
||||
|
||||
/** Additions to BouncyCastle providing Ed25519 to X25519 key conversion. */
|
||||
public class BouncyCastle25519 {
|
||||
|
||||
private static final Class<?> pointAffineClass;
|
||||
private static final Constructor<?> pointAffineCtor;
|
||||
private static final Method decodePointVarMethod;
|
||||
private static final Field yField;
|
||||
|
||||
static {
|
||||
try {
|
||||
Class<?> ed25519Class = Ed25519.class;
|
||||
pointAffineClass = Arrays.stream(ed25519Class.getDeclaredClasses()).filter(clazz -> clazz.getSimpleName().equals("PointAffine")).findFirst().get();
|
||||
if (pointAffineClass == null)
|
||||
throw new ClassNotFoundException("Can't locate PointExt inner class inside Ed25519");
|
||||
|
||||
decodePointVarMethod = ed25519Class.getDeclaredMethod("decodePointVar", byte[].class, int.class, boolean.class, pointAffineClass);
|
||||
decodePointVarMethod.setAccessible(true);
|
||||
|
||||
pointAffineCtor = pointAffineClass.getDeclaredConstructors()[0];
|
||||
pointAffineCtor.setAccessible(true);
|
||||
|
||||
yField = pointAffineClass.getDeclaredField("y");
|
||||
yField.setAccessible(true);
|
||||
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException | NoSuchFieldException | ClassNotFoundException e) {
|
||||
throw new RuntimeException("Can't initialize BouncyCastle25519 shim", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] obtainYFromPublicKey(byte[] ed25519PublicKey) {
|
||||
try {
|
||||
Object pA = pointAffineCtor.newInstance();
|
||||
|
||||
Boolean result = (Boolean) decodePointVarMethod.invoke(null, ed25519PublicKey, 0, true, pA);
|
||||
if (result == null || !result)
|
||||
return null;
|
||||
|
||||
return (int[]) yField.get(pA);
|
||||
} catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
throw new RuntimeException("Can't reflect into BouncyCastle", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] toX25519PublicKey(byte[] ed25519PublicKey) {
|
||||
int[] one = new int[X25519Field.SIZE];
|
||||
X25519Field.one(one);
|
||||
|
||||
int[] y = obtainYFromPublicKey(ed25519PublicKey);
|
||||
|
||||
int[] oneMinusY = new int[X25519Field.SIZE];
|
||||
X25519Field.sub(one, y, oneMinusY);
|
||||
|
||||
int[] onePlusY = new int[X25519Field.SIZE];
|
||||
X25519Field.add(one, y, onePlusY);
|
||||
|
||||
int[] oneMinusYInverted = new int[X25519Field.SIZE];
|
||||
X25519Field.inv(oneMinusY, oneMinusYInverted);
|
||||
|
||||
int[] u = new int[X25519Field.SIZE];
|
||||
X25519Field.mul(onePlusY, oneMinusYInverted, u);
|
||||
|
||||
X25519Field.normalize(u);
|
||||
|
||||
byte[] x25519PublicKey = new byte[X25519.SCALAR_SIZE];
|
||||
X25519Field.encode(u, x25519PublicKey, 0);
|
||||
|
||||
return x25519PublicKey;
|
||||
}
|
||||
|
||||
public static byte[] toX25519PrivateKey(byte[] ed25519PrivateKey) {
|
||||
Digest d = Ed25519.createPrehash();
|
||||
byte[] h = new byte[d.getDigestSize()];
|
||||
|
||||
d.update(ed25519PrivateKey, 0, ed25519PrivateKey.length);
|
||||
d.doFinal(h, 0);
|
||||
|
||||
byte[] s = new byte[X25519.SCALAR_SIZE];
|
||||
|
||||
System.arraycopy(h, 0, s, 0, X25519.SCALAR_SIZE);
|
||||
s[0] &= 0xF8;
|
||||
s[X25519.SCALAR_SIZE - 1] &= 0x7F;
|
||||
s[X25519.SCALAR_SIZE - 1] |= 0x40;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
}
|
||||
1427
src/main/java/org/qortal/crypto/BouncyCastleEd25519.java
Normal file
1427
src/main/java/org/qortal/crypto/BouncyCastleEd25519.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -253,6 +253,10 @@ public abstract class Crypto {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static byte[] toPublicKey(byte[] privateKey) {
|
||||
return new Ed25519PrivateKeyParameters(privateKey, 0).generatePublicKey().getEncoded();
|
||||
}
|
||||
|
||||
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
|
||||
try {
|
||||
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
|
||||
@@ -264,16 +268,24 @@ public abstract class Crypto {
|
||||
public static byte[] sign(Ed25519PrivateKeyParameters edPrivateKeyParams, byte[] message) {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
|
||||
edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519, edPrivateKeyParams.generatePublicKey(), null, message, 0, message.length, signature, 0);
|
||||
edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519,null, message, 0, message.length, signature, 0);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static byte[] sign(byte[] privateKey, byte[] message) {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
|
||||
new Ed25519PrivateKeyParameters(privateKey, 0).sign(Ed25519.Algorithm.Ed25519,null, message, 0, message.length, signature, 0);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static byte[] getSharedSecret(byte[] privateKey, byte[] publicKey) {
|
||||
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(privateKey);
|
||||
byte[] x25519PrivateKey = Qortal25519Extras.toX25519PrivateKey(privateKey);
|
||||
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
|
||||
|
||||
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
|
||||
byte[] x25519PublicKey = Qortal25519Extras.toX25519PublicKey(publicKey);
|
||||
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
|
||||
|
||||
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
|
||||
@@ -281,5 +293,4 @@ public abstract class Crypto {
|
||||
|
||||
return sharedSecret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
234
src/main/java/org/qortal/crypto/Qortal25519Extras.java
Normal file
234
src/main/java/org/qortal/crypto/Qortal25519Extras.java
Normal file
@@ -0,0 +1,234 @@
|
||||
package org.qortal.crypto;
|
||||
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.digests.SHA512Digest;
|
||||
import org.bouncycastle.math.ec.rfc7748.X25519;
|
||||
import org.bouncycastle.math.ec.rfc7748.X25519Field;
|
||||
import org.bouncycastle.math.ec.rfc8032.Ed25519;
|
||||
import org.bouncycastle.math.raw.Nat256;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Additions to BouncyCastle providing:
|
||||
* <p></p>
|
||||
* <ul>
|
||||
* <li>Ed25519 to X25519 key conversion</li>
|
||||
* <li>Aggregate public keys</li>
|
||||
* <li>Aggregate signatures</li>
|
||||
* </ul>
|
||||
*/
|
||||
public abstract class Qortal25519Extras extends BouncyCastleEd25519 {
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
public static byte[] toX25519PublicKey(byte[] ed25519PublicKey) {
|
||||
int[] one = new int[X25519Field.SIZE];
|
||||
X25519Field.one(one);
|
||||
|
||||
PointAffine pA = new PointAffine();
|
||||
if (!decodePointVar(ed25519PublicKey, 0, true, pA))
|
||||
return null;
|
||||
|
||||
int[] y = pA.y;
|
||||
|
||||
int[] oneMinusY = new int[X25519Field.SIZE];
|
||||
X25519Field.sub(one, y, oneMinusY);
|
||||
|
||||
int[] onePlusY = new int[X25519Field.SIZE];
|
||||
X25519Field.add(one, y, onePlusY);
|
||||
|
||||
int[] oneMinusYInverted = new int[X25519Field.SIZE];
|
||||
X25519Field.inv(oneMinusY, oneMinusYInverted);
|
||||
|
||||
int[] u = new int[X25519Field.SIZE];
|
||||
X25519Field.mul(onePlusY, oneMinusYInverted, u);
|
||||
|
||||
X25519Field.normalize(u);
|
||||
|
||||
byte[] x25519PublicKey = new byte[X25519.SCALAR_SIZE];
|
||||
X25519Field.encode(u, x25519PublicKey, 0);
|
||||
|
||||
return x25519PublicKey;
|
||||
}
|
||||
|
||||
public static byte[] toX25519PrivateKey(byte[] ed25519PrivateKey) {
|
||||
Digest d = Ed25519.createPrehash();
|
||||
byte[] h = new byte[d.getDigestSize()];
|
||||
|
||||
d.update(ed25519PrivateKey, 0, ed25519PrivateKey.length);
|
||||
d.doFinal(h, 0);
|
||||
|
||||
byte[] s = new byte[X25519.SCALAR_SIZE];
|
||||
|
||||
System.arraycopy(h, 0, s, 0, X25519.SCALAR_SIZE);
|
||||
s[0] &= 0xF8;
|
||||
s[X25519.SCALAR_SIZE - 1] &= 0x7F;
|
||||
s[X25519.SCALAR_SIZE - 1] |= 0x40;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// Mostly for test support
|
||||
public static PointAccum newPointAccum() {
|
||||
return new PointAccum();
|
||||
}
|
||||
|
||||
public static byte[] aggregatePublicKeys(Collection<byte[]> publicKeys) {
|
||||
PointAccum rAccum = null;
|
||||
|
||||
for (byte[] publicKey : publicKeys) {
|
||||
PointAffine pA = new PointAffine();
|
||||
if (!decodePointVar(publicKey, 0, false, pA))
|
||||
// Failed to decode
|
||||
return null;
|
||||
|
||||
if (rAccum == null) {
|
||||
rAccum = new PointAccum();
|
||||
pointCopy(pA, rAccum);
|
||||
} else {
|
||||
pointAdd(pointCopy(pA), rAccum);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] publicKey = new byte[SCALAR_BYTES];
|
||||
if (0 == encodePoint(rAccum, publicKey, 0))
|
||||
// Failed to encode
|
||||
return null;
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public static byte[] aggregateSignatures(Collection<byte[]> signatures) {
|
||||
// Signatures are (R, s)
|
||||
// R is a point
|
||||
// s is a scalar
|
||||
PointAccum rAccum = null;
|
||||
int[] sAccum = new int[SCALAR_INTS];
|
||||
|
||||
byte[] rEncoded = new byte[POINT_BYTES];
|
||||
int[] sPart = new int[SCALAR_INTS];
|
||||
for (byte[] signature : signatures) {
|
||||
System.arraycopy(signature,0, rEncoded, 0, rEncoded.length);
|
||||
|
||||
PointAffine pA = new PointAffine();
|
||||
if (!decodePointVar(rEncoded, 0, false, pA))
|
||||
// Failed to decode
|
||||
return null;
|
||||
|
||||
if (rAccum == null) {
|
||||
rAccum = new PointAccum();
|
||||
pointCopy(pA, rAccum);
|
||||
|
||||
decode32(signature, rEncoded.length, sAccum, 0, SCALAR_INTS);
|
||||
} else {
|
||||
pointAdd(pointCopy(pA), rAccum);
|
||||
|
||||
decode32(signature, rEncoded.length, sPart, 0, SCALAR_INTS);
|
||||
Nat256.addTo(sPart, sAccum);
|
||||
|
||||
// "mod L" on sAccum
|
||||
if (Nat256.gte(sAccum, L))
|
||||
Nat256.subFrom(L, sAccum);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_SIZE];
|
||||
if (0 == encodePoint(rAccum, signature, 0))
|
||||
// Failed to encode
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < sAccum.length; ++i) {
|
||||
encode32(sAccum[i], signature, POINT_BYTES + i * 4);
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static byte[] signForAggregation(byte[] privateKey, byte[] message) {
|
||||
// Very similar to BouncyCastle's implementation except we use secure random nonce and different hash
|
||||
Digest d = new SHA512Digest();
|
||||
byte[] h = new byte[d.getDigestSize()];
|
||||
|
||||
d.reset();
|
||||
d.update(privateKey, 0, privateKey.length);
|
||||
d.doFinal(h, 0);
|
||||
|
||||
byte[] sH = new byte[SCALAR_BYTES];
|
||||
pruneScalar(h, 0, sH);
|
||||
|
||||
byte[] publicKey = new byte[SCALAR_BYTES];
|
||||
scalarMultBaseEncoded(sH, publicKey, 0);
|
||||
|
||||
byte[] rSeed = new byte[d.getDigestSize()];
|
||||
SECURE_RANDOM.nextBytes(rSeed);
|
||||
|
||||
byte[] r = new byte[SCALAR_BYTES];
|
||||
pruneScalar(rSeed, 0, r);
|
||||
|
||||
byte[] R = new byte[POINT_BYTES];
|
||||
scalarMultBaseEncoded(r, R, 0);
|
||||
|
||||
d.reset();
|
||||
d.update(message, 0, message.length);
|
||||
d.doFinal(h, 0);
|
||||
byte[] k = reduceScalar(h);
|
||||
|
||||
byte[] s = calculateS(r, k, sH);
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_SIZE];
|
||||
System.arraycopy(R, 0, signature, 0, POINT_BYTES);
|
||||
System.arraycopy(s, 0, signature, POINT_BYTES, SCALAR_BYTES);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static boolean verifyAggregated(byte[] publicKey, byte[] signature, byte[] message) {
|
||||
byte[] R = Arrays.copyOfRange(signature, 0, POINT_BYTES);
|
||||
|
||||
byte[] s = Arrays.copyOfRange(signature, POINT_BYTES, POINT_BYTES + SCALAR_BYTES);
|
||||
|
||||
if (!checkPointVar(R))
|
||||
// R out of bounds
|
||||
return false;
|
||||
|
||||
if (!checkScalarVar(s))
|
||||
// s out of bounds
|
||||
return false;
|
||||
|
||||
byte[] S = new byte[POINT_BYTES];
|
||||
scalarMultBaseEncoded(s, S, 0);
|
||||
|
||||
PointAffine pA = new PointAffine();
|
||||
if (!decodePointVar(publicKey, 0, true, pA))
|
||||
// Failed to decode
|
||||
return false;
|
||||
|
||||
Digest d = new SHA512Digest();
|
||||
byte[] h = new byte[d.getDigestSize()];
|
||||
|
||||
d.update(message, 0, message.length);
|
||||
d.doFinal(h, 0);
|
||||
|
||||
byte[] k = reduceScalar(h);
|
||||
|
||||
int[] nS = new int[SCALAR_INTS];
|
||||
decodeScalar(s, 0, nS);
|
||||
|
||||
int[] nA = new int[SCALAR_INTS];
|
||||
decodeScalar(k, 0, nA);
|
||||
|
||||
/*PointAccum*/
|
||||
PointAccum pR = new PointAccum();
|
||||
scalarMultStrausVar(nS, nA, pA, pR);
|
||||
|
||||
byte[] check = new byte[POINT_BYTES];
|
||||
if (0 == encodePoint(pR, check, 0))
|
||||
// Failed to encode
|
||||
return false;
|
||||
|
||||
return Arrays.equals(check, R);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class ArbitraryResourceStatus {
|
||||
}
|
||||
}
|
||||
|
||||
private Status status;
|
||||
private String id;
|
||||
private String title;
|
||||
private String description;
|
||||
@@ -37,6 +38,7 @@ public class ArbitraryResourceStatus {
|
||||
}
|
||||
|
||||
public ArbitraryResourceStatus(Status status, Integer localChunkCount, Integer totalChunkCount) {
|
||||
this.status = status;
|
||||
this.id = status.toString();
|
||||
this.title = status.title;
|
||||
this.description = status.description;
|
||||
@@ -47,4 +49,20 @@ public class ArbitraryResourceStatus {
|
||||
public ArbitraryResourceStatus(Status status) {
|
||||
this(status, null, null);
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
public Integer getLocalChunkCount() {
|
||||
return this.localChunkCount;
|
||||
}
|
||||
|
||||
public Integer getTotalChunkCount() {
|
||||
return this.totalChunkCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.Arrays;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
|
||||
@@ -16,6 +17,9 @@ public class OnlineAccountData {
|
||||
protected byte[] signature;
|
||||
protected byte[] publicKey;
|
||||
|
||||
@XmlTransient
|
||||
private int hash;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAXB serialization
|
||||
@@ -62,20 +66,23 @@ public class OnlineAccountData {
|
||||
if (otherOnlineAccountData.timestamp != this.timestamp)
|
||||
return false;
|
||||
|
||||
// Signature more likely to be unique than public key
|
||||
if (!Arrays.equals(otherOnlineAccountData.signature, this.signature))
|
||||
return false;
|
||||
|
||||
if (!Arrays.equals(otherOnlineAccountData.publicKey, this.publicKey))
|
||||
return false;
|
||||
|
||||
// We don't compare signature because it's not our remit to verify and newer aggregate signatures use random nonces
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Pretty lazy implementation
|
||||
return (int) this.timestamp;
|
||||
int h = this.hash;
|
||||
if (h == 0) {
|
||||
this.hash = h = Long.hashCode(this.timestamp)
|
||||
^ Arrays.hashCode(this.publicKey);
|
||||
// We don't use signature because newer aggregate signatures use random nonces
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -469,6 +469,8 @@ public class Network {
|
||||
|
||||
class NetworkProcessor extends ExecuteProduceConsume {
|
||||
|
||||
private final Logger LOGGER = LogManager.getLogger(NetworkProcessor.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
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.message.ChallengeMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.MessageException;
|
||||
import org.qortal.network.message.MessageType;
|
||||
import org.qortal.network.task.MessageTask;
|
||||
import org.qortal.network.task.PingTask;
|
||||
import org.qortal.settings.Settings;
|
||||
@@ -26,6 +27,8 @@ import java.nio.channels.SocketChannel;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.LongAccumulator;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -152,6 +155,16 @@ public class Peer {
|
||||
*/
|
||||
private CommonBlockData commonBlockData;
|
||||
|
||||
// Message stats
|
||||
|
||||
private static class MessageStats {
|
||||
public final LongAdder count = new LongAdder();
|
||||
public final LongAdder totalBytes = new LongAdder();
|
||||
}
|
||||
|
||||
private final Map<MessageType, MessageStats> receivedMessageStats = new ConcurrentHashMap<>();
|
||||
private final Map<MessageType, MessageStats> sentMessageStats = new ConcurrentHashMap<>();
|
||||
|
||||
// Constructors
|
||||
|
||||
/**
|
||||
@@ -541,11 +554,22 @@ public class Peer {
|
||||
// Tidy up buffers:
|
||||
this.byteBuffer.flip();
|
||||
// Read-only, flipped buffer's position will be after end of message, so copy that
|
||||
long messageByteSize = readOnlyBuffer.position();
|
||||
this.byteBuffer.position(readOnlyBuffer.position());
|
||||
// Copy bytes after read message to front of buffer,
|
||||
// adjusting position accordingly, reset limit to capacity
|
||||
this.byteBuffer.compact();
|
||||
|
||||
// Record message stats
|
||||
MessageStats messageStats = this.receivedMessageStats.computeIfAbsent(message.getType(), k -> new MessageStats());
|
||||
// Ideally these two operations would be atomic, we could pack 'count' in top X bits of the 64-bit long, but meh
|
||||
messageStats.count.increment();
|
||||
messageStats.totalBytes.add(messageByteSize);
|
||||
|
||||
// Unsupported message type? Discard with no further processing
|
||||
if (message.getType() == MessageType.UNSUPPORTED)
|
||||
continue;
|
||||
|
||||
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
|
||||
if (queue != null) {
|
||||
// Adding message to queue will unblock thread waiting for response
|
||||
@@ -604,6 +628,12 @@ public class Peer {
|
||||
|
||||
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}",
|
||||
this.peerConnectionId, this.outputMessageType, this.outputMessageId, this);
|
||||
|
||||
// Record message stats
|
||||
MessageStats messageStats = this.sentMessageStats.computeIfAbsent(message.getType(), k -> new MessageStats());
|
||||
// Ideally these two operations would be atomic, we could pack 'count' in top X bits of the 64-bit long, but meh
|
||||
messageStats.count.increment();
|
||||
messageStats.totalBytes.add(this.outputBuffer.limit());
|
||||
} catch (MessageException e) {
|
||||
// Something went wrong converting message to bytes, so discard but allow another round
|
||||
LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
|
||||
@@ -794,8 +824,11 @@ public class Peer {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
boolean logStats = false;
|
||||
|
||||
if (!isStopping) {
|
||||
LOGGER.debug("[{}] Shutting down peer {}", this.peerConnectionId, this);
|
||||
logStats = true;
|
||||
}
|
||||
isStopping = true;
|
||||
|
||||
@@ -807,8 +840,34 @@ public class Peer {
|
||||
LOGGER.debug("[{}] IOException while trying to close peer {}", this.peerConnectionId, this);
|
||||
}
|
||||
}
|
||||
|
||||
if (logStats && this.receivedMessageStats.size() > 0) {
|
||||
StringBuilder statsBuilder = new StringBuilder(1024);
|
||||
statsBuilder.append("peer ").append(this).append(" message stats:\n=received=");
|
||||
appendMessageStats(statsBuilder, this.receivedMessageStats);
|
||||
statsBuilder.append("\n=sent=");
|
||||
appendMessageStats(statsBuilder, this.sentMessageStats);
|
||||
|
||||
LOGGER.debug(statsBuilder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendMessageStats(StringBuilder statsBuilder, Map<MessageType, MessageStats> messageStats) {
|
||||
if (messageStats.isEmpty()) {
|
||||
statsBuilder.append("\n none");
|
||||
return;
|
||||
}
|
||||
|
||||
messageStats.keySet().stream()
|
||||
.sorted(Comparator.comparing(MessageType::name))
|
||||
.forEach(messageType -> {
|
||||
MessageStats stats = messageStats.get(messageType);
|
||||
|
||||
statsBuilder.append("\n ").append(messageType.name())
|
||||
.append(": count=").append(stats.count.sum())
|
||||
.append(", total bytes=").append(stats.totalBytes.sum());
|
||||
});
|
||||
}
|
||||
|
||||
// Minimum version
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
@@ -46,12 +47,12 @@ public class BlockMessage extends Message {
|
||||
try {
|
||||
int height = byteBuffer.getInt();
|
||||
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = BlockTransformer.fromByteBuffer(byteBuffer);
|
||||
BlockTransformation blockTransformation = BlockTransformer.fromByteBuffer(byteBuffer);
|
||||
|
||||
BlockData blockData = blockInfo.getA();
|
||||
BlockData blockData = blockTransformation.getBlockData();
|
||||
blockData.setHeight(height);
|
||||
|
||||
return new BlockMessage(id, blockData, blockInfo.getB(), blockInfo.getC());
|
||||
return new BlockMessage(id, blockData, blockTransformation.getTransactions(), blockTransformation.getAtStates());
|
||||
} catch (TransformationException e) {
|
||||
LOGGER.info(String.format("Received garbled BLOCK message: %s", e.getMessage()));
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
|
||||
87
src/main/java/org/qortal/network/message/BlockV2Message.java
Normal file
87
src/main/java/org/qortal/network/message/BlockV2Message.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
public class BlockV2Message extends Message {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockV2Message.class);
|
||||
public static final long MIN_PEER_VERSION = 0x300030003L; // 3.3.3
|
||||
|
||||
private BlockData blockData;
|
||||
private List<TransactionData> transactions;
|
||||
private byte[] atStatesHash;
|
||||
|
||||
public BlockV2Message(Block block) throws TransformationException {
|
||||
super(MessageType.BLOCK_V2);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
|
||||
bytes.write(BlockTransformer.toBytesV2(block));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public BlockV2Message(byte[] cachedBytes) {
|
||||
super(MessageType.BLOCK_V2);
|
||||
|
||||
this.dataBytes = cachedBytes;
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private BlockV2Message(int id, BlockData blockData, List<TransactionData> transactions, byte[] atStatesHash) {
|
||||
super(id, MessageType.BLOCK_V2);
|
||||
|
||||
this.blockData = blockData;
|
||||
this.transactions = transactions;
|
||||
this.atStatesHash = atStatesHash;
|
||||
}
|
||||
|
||||
public BlockData getBlockData() {
|
||||
return this.blockData;
|
||||
}
|
||||
|
||||
public List<TransactionData> getTransactions() {
|
||||
return this.transactions;
|
||||
}
|
||||
|
||||
public byte[] getAtStatesHash() {
|
||||
return this.atStatesHash;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
try {
|
||||
int height = byteBuffer.getInt();
|
||||
|
||||
BlockTransformation blockTransformation = BlockTransformer.fromByteBufferV2(byteBuffer);
|
||||
|
||||
BlockData blockData = blockTransformation.getBlockData();
|
||||
blockData.setHeight(height);
|
||||
|
||||
return new BlockV2Message(id, blockData, blockTransformation.getTransactions(), blockTransformation.getAtStatesHash());
|
||||
} catch (TransformationException e) {
|
||||
LOGGER.info(String.format("Received garbled BLOCK_V2 message: %s", e.getMessage()));
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* For requesting online accounts info from remote peer, given our list of online accounts.
|
||||
* <p></p>
|
||||
* Different format to V1 and V2:<br>
|
||||
* <ul>
|
||||
* <li>V1 is: number of entries, then timestamp + pubkey for each entry</li>
|
||||
* <li>V2 is: groups of: number of entries, timestamp, then pubkey for each entry</li>
|
||||
* <li>V3 is: groups of: timestamp, number of entries (one per leading byte), then hash(pubkeys) for each entry</li>
|
||||
* </ul>
|
||||
* <p></p>
|
||||
* End
|
||||
*/
|
||||
public class GetOnlineAccountsV3Message extends Message {
|
||||
|
||||
private static final Map<Long, Map<Byte, byte[]>> EMPTY_ONLINE_ACCOUNTS = Collections.emptyMap();
|
||||
private Map<Long, Map<Byte, byte[]>> hashesByTimestampThenByte;
|
||||
|
||||
public GetOnlineAccountsV3Message(Map<Long, Map<Byte, byte[]>> hashesByTimestampThenByte) {
|
||||
super(MessageType.GET_ONLINE_ACCOUNTS_V3);
|
||||
|
||||
// If we don't have ANY online accounts then it's an easier construction...
|
||||
if (hashesByTimestampThenByte.isEmpty()) {
|
||||
this.dataBytes = EMPTY_DATA_BYTES;
|
||||
return;
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = hashesByTimestampThenByte.size() * (Transformer.TIMESTAMP_LENGTH + Transformer.BYTE_LENGTH);
|
||||
|
||||
byteSize += hashesByTimestampThenByte.values()
|
||||
.stream()
|
||||
.mapToInt(map -> map.size() * Transformer.PUBLIC_KEY_LENGTH)
|
||||
.sum();
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
// Warning: no double-checking/fetching! We must be ConcurrentMap compatible.
|
||||
// So no contains() then get() or multiple get()s on the same key/map.
|
||||
try {
|
||||
for (var outerMapEntry : hashesByTimestampThenByte.entrySet()) {
|
||||
bytes.write(Longs.toByteArray(outerMapEntry.getKey()));
|
||||
|
||||
var innerMap = outerMapEntry.getValue();
|
||||
|
||||
// Number of entries: 1 - 256, where 256 is represented by 0
|
||||
bytes.write(innerMap.size() & 0xFF);
|
||||
|
||||
for (byte[] hashBytes : innerMap.values()) {
|
||||
bytes.write(hashBytes);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetOnlineAccountsV3Message(int id, Map<Long, Map<Byte, byte[]>> hashesByTimestampThenByte) {
|
||||
super(id, MessageType.GET_ONLINE_ACCOUNTS_V3);
|
||||
|
||||
this.hashesByTimestampThenByte = hashesByTimestampThenByte;
|
||||
}
|
||||
|
||||
public Map<Long, Map<Byte, byte[]>> getHashesByTimestampThenByte() {
|
||||
return this.hashesByTimestampThenByte;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
// 'empty' case
|
||||
if (!bytes.hasRemaining()) {
|
||||
return new GetOnlineAccountsV3Message(id, EMPTY_ONLINE_ACCOUNTS);
|
||||
}
|
||||
|
||||
Map<Long, Map<Byte, byte[]>> hashesByTimestampThenByte = new HashMap<>();
|
||||
|
||||
while (bytes.hasRemaining()) {
|
||||
long timestamp = bytes.getLong();
|
||||
|
||||
int hashCount = bytes.get();
|
||||
if (hashCount <= 0)
|
||||
// 256 is represented by 0.
|
||||
// Also converts negative signed value (e.g. -1) to proper positive unsigned value (255)
|
||||
hashCount += 256;
|
||||
|
||||
Map<Byte, byte[]> hashesByByte = new HashMap<>();
|
||||
|
||||
for (int i = 0; i < hashCount; ++i) {
|
||||
byte[] publicKeyHash = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
bytes.get(publicKeyHash);
|
||||
|
||||
hashesByByte.put(publicKeyHash[0], publicKeyHash);
|
||||
}
|
||||
|
||||
hashesByTimestampThenByte.put(timestamp, hashesByByte);
|
||||
}
|
||||
|
||||
return new GetOnlineAccountsV3Message(id, hashesByTimestampThenByte);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -46,6 +46,7 @@ public abstract class Message {
|
||||
private static final int MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
protected static final byte[] EMPTY_DATA_BYTES = new byte[0];
|
||||
private static final ByteBuffer EMPTY_READ_ONLY_BYTE_BUFFER = ByteBuffer.wrap(EMPTY_DATA_BYTES).asReadOnlyBuffer();
|
||||
|
||||
protected int id;
|
||||
protected final MessageType type;
|
||||
@@ -103,8 +104,7 @@ public abstract class Message {
|
||||
int typeValue = readOnlyBuffer.getInt();
|
||||
MessageType messageType = MessageType.valueOf(typeValue);
|
||||
if (messageType == null)
|
||||
// Unrecognised message type
|
||||
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
|
||||
messageType = MessageType.UNSUPPORTED;
|
||||
|
||||
// Optional message ID
|
||||
byte hasId = readOnlyBuffer.get();
|
||||
@@ -127,7 +127,7 @@ public abstract class Message {
|
||||
if (dataSize > 0 && dataSize + CHECKSUM_LENGTH > readOnlyBuffer.remaining())
|
||||
return null;
|
||||
|
||||
ByteBuffer dataSlice = null;
|
||||
ByteBuffer dataSlice = EMPTY_READ_ONLY_BYTE_BUFFER;
|
||||
if (dataSize > 0) {
|
||||
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
|
||||
readOnlyBuffer.get(expectedChecksum);
|
||||
|
||||
@@ -8,6 +8,9 @@ import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
public enum MessageType {
|
||||
// Pseudo-message, not sent over the wire
|
||||
UNSUPPORTED(-1, UnsupportedMessage::fromByteBuffer),
|
||||
|
||||
// Handshaking
|
||||
HELLO(0, HelloMessage::fromByteBuffer),
|
||||
GOODBYE(1, GoodbyeMessage::fromByteBuffer),
|
||||
@@ -31,6 +34,7 @@ public enum MessageType {
|
||||
|
||||
BLOCK(50, BlockMessage::fromByteBuffer),
|
||||
GET_BLOCK(51, GetBlockMessage::fromByteBuffer),
|
||||
BLOCK_V2(52, BlockV2Message::fromByteBuffer),
|
||||
|
||||
SIGNATURES(60, SignaturesMessage::fromByteBuffer),
|
||||
GET_SIGNATURES_V2(61, GetSignaturesV2Message::fromByteBuffer),
|
||||
@@ -42,6 +46,8 @@ public enum MessageType {
|
||||
GET_ONLINE_ACCOUNTS(81, GetOnlineAccountsMessage::fromByteBuffer),
|
||||
ONLINE_ACCOUNTS_V2(82, OnlineAccountsV2Message::fromByteBuffer),
|
||||
GET_ONLINE_ACCOUNTS_V2(83, GetOnlineAccountsV2Message::fromByteBuffer),
|
||||
// ONLINE_ACCOUNTS_V3(84, OnlineAccountsV3Message::fromByteBuffer),
|
||||
GET_ONLINE_ACCOUNTS_V3(85, GetOnlineAccountsV3Message::fromByteBuffer),
|
||||
|
||||
ARBITRARY_DATA(90, ArbitraryDataMessage::fromByteBuffer),
|
||||
GET_ARBITRARY_DATA(91, GetArbitraryDataMessage::fromByteBuffer),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class UnsupportedMessage extends Message {
|
||||
|
||||
public UnsupportedMessage() {
|
||||
super(MessageType.UNSUPPORTED);
|
||||
throw new UnsupportedOperationException("Unsupported message is unsupported!");
|
||||
}
|
||||
|
||||
private UnsupportedMessage(int id) {
|
||||
super(id, MessageType.UNSUPPORTED);
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
return new UnsupportedMessage(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -159,6 +159,9 @@ public interface AccountRepository {
|
||||
/** Returns number of active reward-shares involving passed public key as the minting account only. */
|
||||
public int countRewardShares(byte[] mintingAccountPublicKey) throws DataException;
|
||||
|
||||
/** Returns number of active self-shares involving passed public key as the minting account only. */
|
||||
public int countSelfShares(byte[] mintingAccountPublicKey) throws DataException;
|
||||
|
||||
public List<RewardShareData> getRewardShares() throws DataException;
|
||||
|
||||
public List<RewardShareData> findRewardShares(List<String> mintingAccounts, List<String> recipientAccounts, List<String> involvedAddresses, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
@@ -66,7 +67,7 @@ public class BlockArchiveReader {
|
||||
this.fileListCache = Map.copyOf(map);
|
||||
}
|
||||
|
||||
public Triple<BlockData, List<TransactionData>, List<ATStateData>> fetchBlockAtHeight(int height) {
|
||||
public BlockTransformation fetchBlockAtHeight(int height) {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
@@ -77,13 +78,13 @@ public class BlockArchiveReader {
|
||||
}
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(serializedBytes);
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = null;
|
||||
BlockTransformation blockInfo = null;
|
||||
try {
|
||||
blockInfo = BlockTransformer.fromByteBuffer(byteBuffer);
|
||||
if (blockInfo != null && blockInfo.getA() != null) {
|
||||
if (blockInfo != null && blockInfo.getBlockData() != null) {
|
||||
// Block height is stored outside of the main serialized bytes, so it
|
||||
// won't be set automatically.
|
||||
blockInfo.getA().setHeight(height);
|
||||
blockInfo.getBlockData().setHeight(height);
|
||||
}
|
||||
} catch (TransformationException e) {
|
||||
return null;
|
||||
@@ -91,8 +92,7 @@ public class BlockArchiveReader {
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
public Triple<BlockData, List<TransactionData>, List<ATStateData>> fetchBlockWithSignature(
|
||||
byte[] signature, Repository repository) {
|
||||
public BlockTransformation fetchBlockWithSignature(byte[] signature, Repository repository) {
|
||||
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
@@ -105,13 +105,12 @@ public class BlockArchiveReader {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Triple<BlockData, List<TransactionData>, List<ATStateData>>> fetchBlocksFromRange(
|
||||
int startHeight, int endHeight) {
|
||||
public List<BlockTransformation> fetchBlocksFromRange(int startHeight, int endHeight) {
|
||||
|
||||
List<Triple<BlockData, List<TransactionData>, List<ATStateData>>> blockInfoList = new ArrayList<>();
|
||||
List<BlockTransformation> blockInfoList = new ArrayList<>();
|
||||
|
||||
for (int height = startHeight; height <= endHeight; height++) {
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = this.fetchBlockAtHeight(height);
|
||||
BlockTransformation blockInfo = this.fetchBlockAtHeight(height);
|
||||
if (blockInfo == null) {
|
||||
return blockInfoList;
|
||||
}
|
||||
|
||||
@@ -688,6 +688,17 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countSelfShares(byte[] minterPublicKey) throws DataException {
|
||||
String sql = "SELECT COUNT(*) FROM RewardShares WHERE minter_public_key = ? AND minter = recipient";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, minterPublicKey)) {
|
||||
return resultSet.getInt(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to count self-shares in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RewardShareData> getRewardShares() throws DataException {
|
||||
String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares";
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.BlockSignerSummary;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.block.BlockArchiveData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.repository.BlockArchiveReader;
|
||||
import org.qortal.repository.BlockArchiveRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.utils.Triple;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@@ -29,11 +26,11 @@ public class HSQLDBBlockArchiveRepository implements BlockArchiveRepository {
|
||||
|
||||
@Override
|
||||
public BlockData fromSignature(byte[] signature) throws DataException {
|
||||
Triple blockInfo = BlockArchiveReader.getInstance().fetchBlockWithSignature(signature, this.repository);
|
||||
if (blockInfo != null) {
|
||||
return (BlockData) blockInfo.getA();
|
||||
}
|
||||
return null;
|
||||
BlockTransformation blockInfo = BlockArchiveReader.getInstance().fetchBlockWithSignature(signature, this.repository);
|
||||
if (blockInfo == null)
|
||||
return null;
|
||||
|
||||
return blockInfo.getBlockData();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,11 +44,11 @@ public class HSQLDBBlockArchiveRepository implements BlockArchiveRepository {
|
||||
|
||||
@Override
|
||||
public BlockData fromHeight(int height) throws DataException {
|
||||
Triple blockInfo = BlockArchiveReader.getInstance().fetchBlockAtHeight(height);
|
||||
if (blockInfo != null) {
|
||||
return (BlockData) blockInfo.getA();
|
||||
}
|
||||
return null;
|
||||
BlockTransformation blockInfo = BlockArchiveReader.getInstance().fetchBlockAtHeight(height);
|
||||
if (blockInfo == null)
|
||||
return null;
|
||||
|
||||
return blockInfo.getBlockData();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -79,9 +76,9 @@ public class HSQLDBBlockArchiveRepository implements BlockArchiveRepository {
|
||||
int height = referenceBlock.getHeight();
|
||||
if (height > 0) {
|
||||
// Request the block at height + 1
|
||||
Triple blockInfo = BlockArchiveReader.getInstance().fetchBlockAtHeight(height + 1);
|
||||
BlockTransformation blockInfo = BlockArchiveReader.getInstance().fetchBlockAtHeight(height + 1);
|
||||
if (blockInfo != null) {
|
||||
return (BlockData) blockInfo.getA();
|
||||
return blockInfo.getBlockData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,6 +964,17 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("DROP TABLE ArbitraryPeers");
|
||||
break;
|
||||
|
||||
case 42:
|
||||
// We need more space for online accounts
|
||||
stmt.execute("ALTER TABLE Blocks ALTER COLUMN online_accounts SET DATA TYPE VARBINARY(10240)");
|
||||
break;
|
||||
|
||||
case 43:
|
||||
// Pirate Chain requires storing addresses that are 78 bytes long (69 bytes when decoded), so increase
|
||||
// from 32 to 128 to give some padding for potentially even larger addresses in the future
|
||||
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN receiving_account_info SET DATA TYPE VARBINARY(128)");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
||||
@@ -23,7 +23,6 @@ import java.util.stream.Stream;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.gui.SysTray;
|
||||
@@ -1003,7 +1002,7 @@ public class HSQLDBRepository implements Repository {
|
||||
if (privateKey == null)
|
||||
return null;
|
||||
|
||||
return PrivateKeyAccount.toPublicKey(privateKey);
|
||||
return Crypto.toPublicKey(privateKey);
|
||||
}
|
||||
|
||||
public static String ed25519PublicKeyToAddress(byte[] publicKey) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.qortal.crosschain.Litecoin.LitecoinNet;
|
||||
import org.qortal.crosschain.Dogecoin.DogecoinNet;
|
||||
import org.qortal.crosschain.Digibyte.DigibyteNet;
|
||||
import org.qortal.crosschain.Ravencoin.RavencoinNet;
|
||||
import org.qortal.crosschain.PirateChain.PirateChainNet;
|
||||
import org.qortal.utils.EnumUtils;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@@ -203,16 +204,16 @@ public class Settings {
|
||||
private int maxRetries = 2;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "3.1.0";
|
||||
private String minPeerVersion = "3.3.7";
|
||||
/** Whether to allow connections with peers below minPeerVersion
|
||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||
private boolean allowConnectionsWithOlderPeerVersions = true;
|
||||
|
||||
/** Minimum time (in seconds) that we should attempt to remain connected to a peer for */
|
||||
private int minPeerConnectionTime = 5 * 60; // seconds
|
||||
private int minPeerConnectionTime = 60 * 60; // seconds
|
||||
/** Maximum time (in seconds) that we should attempt to remain connected to a peer for */
|
||||
private int maxPeerConnectionTime = 60 * 60; // seconds
|
||||
private int maxPeerConnectionTime = 4 * 60 * 60; // seconds
|
||||
/** Maximum time (in seconds) that a peer should remain connected when requesting QDN data */
|
||||
private int maxDataPeerConnectionTime = 2 * 60; // seconds
|
||||
|
||||
@@ -232,10 +233,16 @@ public class Settings {
|
||||
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
|
||||
private DigibyteNet digibyteNet = DigibyteNet.MAIN;
|
||||
private RavencoinNet ravencoinNet = RavencoinNet.MAIN;
|
||||
private PirateChainNet pirateChainNet = PirateChainNet.MAIN;
|
||||
// Also crosschain-related:
|
||||
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
||||
private boolean tradebotSystrayEnabled = false;
|
||||
|
||||
/** Wallets path - used for storing encrypted wallet caches for coins that require them */
|
||||
private String walletsPath = "wallets";
|
||||
|
||||
private int arrrDefaultBirthday = 2000000;
|
||||
|
||||
// Repository related
|
||||
/** Queries that take longer than this are logged. (milliseconds) */
|
||||
private Long slowQueryThreshold = null;
|
||||
@@ -284,6 +291,16 @@ public class Settings {
|
||||
private Long testNtpOffset = null;
|
||||
|
||||
|
||||
/* Foreign chains */
|
||||
|
||||
/** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */
|
||||
private int gapLimit = 24;
|
||||
|
||||
/** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */
|
||||
private int bitcoinjLookaheadSize = 50;
|
||||
|
||||
|
||||
|
||||
// Data storage (QDN)
|
||||
|
||||
/** Data storage enabled/disabled*/
|
||||
@@ -706,6 +723,18 @@ public class Settings {
|
||||
return this.ravencoinNet;
|
||||
}
|
||||
|
||||
public PirateChainNet getPirateChainNet() {
|
||||
return this.pirateChainNet;
|
||||
}
|
||||
|
||||
public String getWalletsPath() {
|
||||
return this.walletsPath;
|
||||
}
|
||||
|
||||
public int getArrrDefaultBirthday() {
|
||||
return this.arrrDefaultBirthday;
|
||||
}
|
||||
|
||||
public boolean isTradebotSystrayEnabled() {
|
||||
return this.tradebotSystrayEnabled;
|
||||
}
|
||||
@@ -872,6 +901,15 @@ public class Settings {
|
||||
}
|
||||
|
||||
|
||||
public int getGapLimit() {
|
||||
return this.gapLimit;
|
||||
}
|
||||
|
||||
public int getBitcoinjLookaheadSize() {
|
||||
return bitcoinjLookaheadSize;
|
||||
}
|
||||
|
||||
|
||||
public boolean isQdnEnabled() {
|
||||
return this.qdnEnabled;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.util.function.Supplier;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
@@ -49,7 +48,7 @@ public class PresenceTransaction extends Transaction {
|
||||
REWARD_SHARE(0) {
|
||||
@Override
|
||||
public long getLifetime() {
|
||||
return OnlineAccountsManager.ONLINE_TIMESTAMP_MODULUS;
|
||||
return OnlineAccountsManager.getOnlineTimestampModulus();
|
||||
}
|
||||
},
|
||||
TRADE_BOT(1) {
|
||||
|
||||
@@ -140,8 +140,21 @@ public class RewardShareTransaction extends Transaction {
|
||||
|
||||
// Check the minting account hasn't reach maximum number of reward-shares
|
||||
int rewardShareCount = this.repository.getAccountRepository().countRewardShares(creator.getPublicKey());
|
||||
if (rewardShareCount >= BlockChain.getInstance().getMaxRewardSharesPerMintingAccount())
|
||||
int selfShareCount = this.repository.getAccountRepository().countSelfShares(creator.getPublicKey());
|
||||
|
||||
int maxRewardShares = BlockChain.getInstance().getMaxRewardSharesAtTimestamp(this.rewardShareTransactionData.getTimestamp());
|
||||
if (creator.isFounder())
|
||||
// Founders have a different limit
|
||||
maxRewardShares = BlockChain.getInstance().getMaxRewardSharesPerFounderMintingAccount();
|
||||
|
||||
if (rewardShareCount >= maxRewardShares)
|
||||
return ValidationResult.MAXIMUM_REWARD_SHARES;
|
||||
|
||||
// When filling all reward share slots, one must be a self share (after feature trigger timestamp)
|
||||
if (this.rewardShareTransactionData.getTimestamp() >= BlockChain.getInstance().getRewardShareLimitTimestamp())
|
||||
if (!isRecipientAlsoMinter && rewardShareCount == maxRewardShares-1 && selfShareCount == 0)
|
||||
return ValidationResult.MAXIMUM_REWARD_SHARES;
|
||||
|
||||
} else {
|
||||
// This transaction intends to modify/terminate an existing reward-share
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.qortal.transform.block;
|
||||
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BlockTransformation {
|
||||
private final BlockData blockData;
|
||||
private final List<TransactionData> transactions;
|
||||
private final List<ATStateData> atStates;
|
||||
private final byte[] atStatesHash;
|
||||
|
||||
/*package*/ BlockTransformation(BlockData blockData, List<TransactionData> transactions, List<ATStateData> atStates) {
|
||||
this.blockData = blockData;
|
||||
this.transactions = transactions;
|
||||
this.atStates = atStates;
|
||||
this.atStatesHash = null;
|
||||
}
|
||||
|
||||
/*package*/ BlockTransformation(BlockData blockData, List<TransactionData> transactions, byte[] atStatesHash) {
|
||||
this.blockData = blockData;
|
||||
this.transactions = transactions;
|
||||
this.atStates = null;
|
||||
this.atStatesHash = atStatesHash;
|
||||
}
|
||||
|
||||
public BlockData getBlockData() {
|
||||
return blockData;
|
||||
}
|
||||
|
||||
public List<TransactionData> getTransactions() {
|
||||
return transactions;
|
||||
}
|
||||
|
||||
public List<ATStateData> getAtStates() {
|
||||
return atStates;
|
||||
}
|
||||
|
||||
public byte[] getAtStatesHash() {
|
||||
return atStatesHash;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ package org.qortal.transform.block;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -20,7 +22,6 @@ import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Serialization;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
@@ -45,14 +46,13 @@ public class BlockTransformer extends Transformer {
|
||||
|
||||
protected static final int AT_BYTES_LENGTH = INT_LENGTH;
|
||||
protected static final int AT_FEES_LENGTH = AMOUNT_LENGTH;
|
||||
protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH;
|
||||
|
||||
protected static final int ONLINE_ACCOUNTS_COUNT_LENGTH = INT_LENGTH;
|
||||
protected static final int ONLINE_ACCOUNTS_SIZE_LENGTH = INT_LENGTH;
|
||||
protected static final int ONLINE_ACCOUNTS_TIMESTAMP_LENGTH = TIMESTAMP_LENGTH;
|
||||
protected static final int ONLINE_ACCOUNTS_SIGNATURES_COUNT_LENGTH = INT_LENGTH;
|
||||
|
||||
protected static final int AT_ENTRY_LENGTH = ADDRESS_LENGTH + SHA256_LENGTH + AMOUNT_LENGTH;
|
||||
public static final int AT_ENTRY_LENGTH = ADDRESS_LENGTH + SHA256_LENGTH + AMOUNT_LENGTH;
|
||||
|
||||
/**
|
||||
* Extract block data and transaction data from serialized bytes.
|
||||
@@ -61,7 +61,7 @@ public class BlockTransformer extends Transformer {
|
||||
* @return BlockData and a List of transactions.
|
||||
* @throws TransformationException
|
||||
*/
|
||||
public static Triple<BlockData, List<TransactionData>, List<ATStateData>> fromBytes(byte[] bytes) throws TransformationException {
|
||||
public static BlockTransformation fromBytes(byte[] bytes) throws TransformationException {
|
||||
if (bytes == null)
|
||||
return null;
|
||||
|
||||
@@ -76,28 +76,40 @@ public class BlockTransformer extends Transformer {
|
||||
/**
|
||||
* Extract block data and transaction data from serialized bytes containing a single block.
|
||||
*
|
||||
* @param bytes
|
||||
* @param byteBuffer source of serialized block bytes
|
||||
* @return BlockData and a List of transactions.
|
||||
* @throws TransformationException
|
||||
*/
|
||||
public static Triple<BlockData, List<TransactionData>, List<ATStateData>> fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
|
||||
public static BlockTransformation fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
|
||||
return BlockTransformer.fromByteBuffer(byteBuffer, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract block data and transaction data from serialized bytes containing a single block.
|
||||
*
|
||||
* @param byteBuffer source of serialized block bytes
|
||||
* @return BlockData and a List of transactions.
|
||||
* @throws TransformationException
|
||||
*/
|
||||
public static BlockTransformation fromByteBufferV2(ByteBuffer byteBuffer) throws TransformationException {
|
||||
return BlockTransformer.fromByteBuffer(byteBuffer, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract block data and transaction data from serialized bytes containing one or more blocks.
|
||||
*
|
||||
* @param bytes
|
||||
* Extract block data and transaction data from serialized bytes containing a single block, in one of two forms.
|
||||
*
|
||||
* @param byteBuffer source of serialized block bytes
|
||||
* @param isV2 set to true if AT state info is represented by a single hash, false if serialized as per-AT address+state hash+fees
|
||||
* @return the next block's BlockData and a List of transactions.
|
||||
* @throws TransformationException
|
||||
*/
|
||||
public static Triple<BlockData, List<TransactionData>, List<ATStateData>> fromByteBuffer(ByteBuffer byteBuffer, boolean finalBlockInBuffer) throws TransformationException {
|
||||
private static BlockTransformation fromByteBuffer(ByteBuffer byteBuffer, boolean isV2) throws TransformationException {
|
||||
int version = byteBuffer.getInt();
|
||||
|
||||
if (finalBlockInBuffer && byteBuffer.remaining() < BASE_LENGTH + AT_BYTES_LENGTH - VERSION_LENGTH)
|
||||
if (byteBuffer.remaining() < BASE_LENGTH + AT_BYTES_LENGTH - VERSION_LENGTH)
|
||||
throw new TransformationException("Byte data too short for Block");
|
||||
|
||||
if (finalBlockInBuffer && byteBuffer.remaining() > BlockChain.getInstance().getMaxBlockSize())
|
||||
if (byteBuffer.remaining() > BlockChain.getInstance().getMaxBlockSize())
|
||||
throw new TransformationException("Byte data too long for Block");
|
||||
|
||||
long timestamp = byteBuffer.getLong();
|
||||
@@ -117,42 +129,52 @@ public class BlockTransformer extends Transformer {
|
||||
|
||||
int atCount = 0;
|
||||
long atFees = 0;
|
||||
List<ATStateData> atStates = new ArrayList<>();
|
||||
byte[] atStatesHash = null;
|
||||
List<ATStateData> atStates = null;
|
||||
|
||||
int atBytesLength = byteBuffer.getInt();
|
||||
if (isV2) {
|
||||
// Simply: AT count, AT total fees, hash(all AT states)
|
||||
atCount = byteBuffer.getInt();
|
||||
atFees = byteBuffer.getLong();
|
||||
atStatesHash = new byte[Transformer.SHA256_LENGTH];
|
||||
byteBuffer.get(atStatesHash);
|
||||
} else {
|
||||
// V1: AT info byte length, then per-AT entries of AT address + state hash + fees
|
||||
int atBytesLength = byteBuffer.getInt();
|
||||
if (atBytesLength > BlockChain.getInstance().getMaxBlockSize())
|
||||
throw new TransformationException("Byte data too long for Block's AT info");
|
||||
|
||||
if (atBytesLength > BlockChain.getInstance().getMaxBlockSize())
|
||||
throw new TransformationException("Byte data too long for Block's AT info");
|
||||
// Read AT-address, SHA256 hash and fees
|
||||
if (atBytesLength % AT_ENTRY_LENGTH != 0)
|
||||
throw new TransformationException("AT byte data not a multiple of AT entry length");
|
||||
|
||||
ByteBuffer atByteBuffer = byteBuffer.slice();
|
||||
atByteBuffer.limit(atBytesLength);
|
||||
ByteBuffer atByteBuffer = byteBuffer.slice();
|
||||
atByteBuffer.limit(atBytesLength);
|
||||
|
||||
// Read AT-address, SHA256 hash and fees
|
||||
if (atBytesLength % AT_ENTRY_LENGTH != 0)
|
||||
throw new TransformationException("AT byte data not a multiple of AT entry length");
|
||||
atStates = new ArrayList<>();
|
||||
while (atByteBuffer.hasRemaining()) {
|
||||
byte[] atAddressBytes = new byte[ADDRESS_LENGTH];
|
||||
atByteBuffer.get(atAddressBytes);
|
||||
String atAddress = Base58.encode(atAddressBytes);
|
||||
|
||||
while (atByteBuffer.hasRemaining()) {
|
||||
byte[] atAddressBytes = new byte[ADDRESS_LENGTH];
|
||||
atByteBuffer.get(atAddressBytes);
|
||||
String atAddress = Base58.encode(atAddressBytes);
|
||||
byte[] stateHash = new byte[SHA256_LENGTH];
|
||||
atByteBuffer.get(stateHash);
|
||||
|
||||
byte[] stateHash = new byte[SHA256_LENGTH];
|
||||
atByteBuffer.get(stateHash);
|
||||
long fees = atByteBuffer.getLong();
|
||||
|
||||
long fees = atByteBuffer.getLong();
|
||||
// Add this AT's fees to our total
|
||||
atFees += fees;
|
||||
|
||||
// Add this AT's fees to our total
|
||||
atFees += fees;
|
||||
atStates.add(new ATStateData(atAddress, stateHash, fees));
|
||||
}
|
||||
|
||||
atStates.add(new ATStateData(atAddress, stateHash, fees));
|
||||
// Bump byteBuffer over AT states just read in slice
|
||||
byteBuffer.position(byteBuffer.position() + atBytesLength);
|
||||
|
||||
// AT count to reflect the number of states we have
|
||||
atCount = atStates.size();
|
||||
}
|
||||
|
||||
// Bump byteBuffer over AT states just read in slice
|
||||
byteBuffer.position(byteBuffer.position() + atBytesLength);
|
||||
|
||||
// AT count to reflect the number of states we have
|
||||
atCount = atStates.size();
|
||||
|
||||
// Add AT fees to totalFees
|
||||
totalFees += atFees;
|
||||
|
||||
@@ -221,16 +243,15 @@ public class BlockTransformer extends Transformer {
|
||||
byteBuffer.get(onlineAccountsSignatures);
|
||||
}
|
||||
|
||||
// We should only complain about excess byte data if we aren't expecting more blocks in this ByteBuffer
|
||||
if (finalBlockInBuffer && byteBuffer.hasRemaining())
|
||||
throw new TransformationException("Excess byte data found after parsing Block");
|
||||
|
||||
// We don't have a height!
|
||||
Integer height = null;
|
||||
BlockData blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp,
|
||||
minterPublicKey, minterSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsCount, onlineAccountsTimestamp, onlineAccountsSignatures);
|
||||
|
||||
return new Triple<>(blockData, transactions, atStates);
|
||||
if (isV2)
|
||||
return new BlockTransformation(blockData, transactions, atStatesHash);
|
||||
else
|
||||
return new BlockTransformation(blockData, transactions, atStates);
|
||||
}
|
||||
|
||||
public static int getDataLength(Block block) throws TransformationException {
|
||||
@@ -266,6 +287,14 @@ public class BlockTransformer extends Transformer {
|
||||
}
|
||||
|
||||
public static byte[] toBytes(Block block) throws TransformationException {
|
||||
return toBytes(block, false);
|
||||
}
|
||||
|
||||
public static byte[] toBytesV2(Block block) throws TransformationException {
|
||||
return toBytes(block, true);
|
||||
}
|
||||
|
||||
private static byte[] toBytes(Block block, boolean isV2) throws TransformationException {
|
||||
BlockData blockData = block.getBlockData();
|
||||
|
||||
try {
|
||||
@@ -279,16 +308,37 @@ public class BlockTransformer extends Transformer {
|
||||
bytes.write(blockData.getMinterSignature());
|
||||
|
||||
int atBytesLength = blockData.getATCount() * AT_ENTRY_LENGTH;
|
||||
bytes.write(Ints.toByteArray(atBytesLength));
|
||||
if (isV2) {
|
||||
ByteArrayOutputStream atHashBytes = new ByteArrayOutputStream(atBytesLength);
|
||||
long atFees = 0;
|
||||
|
||||
for (ATStateData atStateData : block.getATStates()) {
|
||||
// Skip initial states generated by DEPLOY_AT transactions in the same block
|
||||
if (atStateData.isInitial())
|
||||
continue;
|
||||
for (ATStateData atStateData : block.getATStates()) {
|
||||
// Skip initial states generated by DEPLOY_AT transactions in the same block
|
||||
if (atStateData.isInitial())
|
||||
continue;
|
||||
|
||||
bytes.write(Base58.decode(atStateData.getATAddress()));
|
||||
bytes.write(atStateData.getStateHash());
|
||||
bytes.write(Longs.toByteArray(atStateData.getFees()));
|
||||
atHashBytes.write(atStateData.getATAddress().getBytes(StandardCharsets.UTF_8));
|
||||
atHashBytes.write(atStateData.getStateHash());
|
||||
atHashBytes.write(Longs.toByteArray(atStateData.getFees()));
|
||||
|
||||
atFees += atStateData.getFees();
|
||||
}
|
||||
|
||||
bytes.write(Ints.toByteArray(blockData.getATCount()));
|
||||
bytes.write(Longs.toByteArray(atFees));
|
||||
bytes.write(Crypto.digest(atHashBytes.toByteArray()));
|
||||
} else {
|
||||
bytes.write(Ints.toByteArray(atBytesLength));
|
||||
|
||||
for (ATStateData atStateData : block.getATStates()) {
|
||||
// Skip initial states generated by DEPLOY_AT transactions in the same block
|
||||
if (atStateData.isInitial())
|
||||
continue;
|
||||
|
||||
bytes.write(Base58.decode(atStateData.getATAddress()));
|
||||
bytes.write(atStateData.getStateHash());
|
||||
bytes.write(Longs.toByteArray(atStateData.getFees()));
|
||||
}
|
||||
}
|
||||
|
||||
// Transactions
|
||||
|
||||
@@ -5,7 +5,10 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFileChunk;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
@@ -410,4 +413,31 @@ public class ArbitraryTransactionUtils {
|
||||
return transactions.stream().skip(offset).limit(limit).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lookup status of resource
|
||||
* @param service
|
||||
* @param name
|
||||
* @param identifier
|
||||
* @param build
|
||||
* @return
|
||||
*/
|
||||
public static ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
|
||||
|
||||
// If "build" has been specified, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
try {
|
||||
if (!reader.isBuilding()) {
|
||||
reader.loadSynchronously(false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// No need to handle exception, as it will be reflected in the status
|
||||
}
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BitTwiddling {
|
||||
|
||||
/**
|
||||
@@ -48,4 +50,25 @@ public class BitTwiddling {
|
||||
| (bytes[start + 4] & 0xffL) << 24 | (bytes[start + 5] & 0xffL) << 16 | (bytes[start + 6] & 0xffL) << 8 | (bytes[start + 7] & 0xffL);
|
||||
}
|
||||
|
||||
/** Convert little-endian bytes to long */
|
||||
public static long longFromLEBytes(byte[] bytes, int start) {
|
||||
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
|
||||
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
|
||||
}
|
||||
|
||||
|
||||
/** Read 8-bit unsigned integer from byte buffer */
|
||||
public static int readU8(ByteBuffer byteBuffer) {
|
||||
byte[] sizeBytes = new byte[1];
|
||||
byteBuffer.get(sizeBytes);
|
||||
return sizeBytes[0] & 0xff;
|
||||
}
|
||||
|
||||
/** Read 32-bit unsigned integer from byte buffer */
|
||||
public static int readU32(ByteBuffer byteBuffer) {
|
||||
byte[] bytes = new byte[4];
|
||||
byteBuffer.get(bytes);
|
||||
return BitTwiddling.intFromLEBytes(bytes, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockArchiveReader;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,8 +34,7 @@ public class BlockArchiveUtils {
|
||||
repository.discardChanges();
|
||||
final int requestedRange = endHeight+1-startHeight;
|
||||
|
||||
List<Triple<BlockData, List<TransactionData>, List<ATStateData>>> blockInfoList =
|
||||
BlockArchiveReader.getInstance().fetchBlocksFromRange(startHeight, endHeight);
|
||||
List<BlockTransformation> blockInfoList = BlockArchiveReader.getInstance().fetchBlocksFromRange(startHeight, endHeight);
|
||||
|
||||
// Ensure that we have received all of the requested blocks
|
||||
if (blockInfoList == null || blockInfoList.isEmpty()) {
|
||||
@@ -43,27 +43,26 @@ public class BlockArchiveUtils {
|
||||
if (blockInfoList.size() != requestedRange) {
|
||||
throw new IllegalStateException("Non matching block count when importing from archive");
|
||||
}
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> firstBlock = blockInfoList.get(0);
|
||||
if (firstBlock == null || firstBlock.getA().getHeight() != startHeight) {
|
||||
BlockTransformation firstBlock = blockInfoList.get(0);
|
||||
if (firstBlock == null || firstBlock.getBlockData().getHeight() != startHeight) {
|
||||
throw new IllegalStateException("Non matching first block when importing from archive");
|
||||
}
|
||||
if (blockInfoList.size() > 0) {
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> lastBlock =
|
||||
blockInfoList.get(blockInfoList.size() - 1);
|
||||
if (lastBlock == null || lastBlock.getA().getHeight() != endHeight) {
|
||||
BlockTransformation lastBlock = blockInfoList.get(blockInfoList.size() - 1);
|
||||
if (lastBlock == null || lastBlock.getBlockData().getHeight() != endHeight) {
|
||||
throw new IllegalStateException("Non matching last block when importing from archive");
|
||||
}
|
||||
}
|
||||
|
||||
// Everything seems okay, so go ahead with the import
|
||||
for (Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo : blockInfoList) {
|
||||
for (BlockTransformation blockInfo : blockInfoList) {
|
||||
try {
|
||||
// Save block
|
||||
repository.getBlockRepository().save(blockInfo.getA());
|
||||
repository.getBlockRepository().save(blockInfo.getBlockData());
|
||||
|
||||
// Save AT state data hashes
|
||||
for (ATStateData atStateData : blockInfo.getC()) {
|
||||
atStateData.setHeight(blockInfo.getA().getHeight());
|
||||
for (ATStateData atStateData : blockInfo.getAtStates()) {
|
||||
atStateData.setHeight(blockInfo.getBlockData().getHeight());
|
||||
repository.getATRepository().save(atStateData);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,15 @@
|
||||
"minAccountLevelToMint": 1,
|
||||
"minAccountLevelForBlockSubmissions": 5,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 6,
|
||||
"maxRewardSharesPerFounderMintingAccount": 6,
|
||||
"maxRewardSharesByTimestamp": [
|
||||
{ "timestamp": 0, "maxShares": 6 },
|
||||
{ "timestamp": 1657382400000, "maxShares": 3 }
|
||||
],
|
||||
"founderEffectiveMintingLevel": 10,
|
||||
"onlineAccountSignaturesMinLifetime": 43200000,
|
||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||
"onlineAccountsModulusV2Timestamp": 1659801600000,
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 5.00 },
|
||||
{ "height": 259201, "reward": 4.75 },
|
||||
@@ -35,14 +40,16 @@
|
||||
{ "height": 3110401, "reward": 2.00 }
|
||||
],
|
||||
"sharesByLevel": [
|
||||
{ "levels": [ 1, 2 ], "share": 0.05 },
|
||||
{ "levels": [ 3, 4 ], "share": 0.10 },
|
||||
{ "levels": [ 5, 6 ], "share": 0.15 },
|
||||
{ "levels": [ 7, 8 ], "share": 0.20 },
|
||||
{ "levels": [ 9, 10 ], "share": 0.25 }
|
||||
{ "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 }
|
||||
],
|
||||
"qoraHoldersShare": 0.20,
|
||||
"qoraPerQortReward": 250,
|
||||
"minAccountsToActivateShareBin": 30,
|
||||
"shareBinActivationMinLevel": 7,
|
||||
"blocksNeededByLevel": [ 7200, 64800, 129600, 172800, 244000, 345600, 518400, 691200, 864000, 1036800 ],
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
@@ -57,10 +64,12 @@
|
||||
"atFindNextTransactionFix": 275000,
|
||||
"newBlockSigHeight": 320000,
|
||||
"shareBinFix": 399000,
|
||||
"rewardShareLimitTimestamp": 1657382400000,
|
||||
"calcChainWeightTimestamp": 1620579600000,
|
||||
"transactionV5Timestamp": 1642176000000,
|
||||
"transactionV6Timestamp": 9999999999999,
|
||||
"disableReferenceTimestamp": 9999999999999
|
||||
"disableReferenceTimestamp": 1655222400000,
|
||||
"aggregateSignatureTimestamp": 1656864000000
|
||||
},
|
||||
"genesisInfo": {
|
||||
"version": 4,
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
### Common ###
|
||||
JSON = JSON Nachricht konnte nicht geparst werden
|
||||
|
||||
INSUFFICIENT_BALANCE = insufficient balance
|
||||
INSUFFICIENT_BALANCE = Kein Ausgleich
|
||||
|
||||
UNAUTHORIZED = API-Aufruf nicht autorisiert
|
||||
|
||||
REPOSITORY_ISSUE = Repository-Fehler
|
||||
|
||||
NON_PRODUCTION = this API call is not permitted for production systems
|
||||
NON_PRODUCTION = Dieser APi-Aufruf ist nicht gestattet für Produtkion
|
||||
|
||||
BLOCKCHAIN_NEEDS_SYNC = blockchain needs to synchronize first
|
||||
BLOCKCHAIN_NEEDS_SYNC = Blockchain muss sich erst verbinden
|
||||
|
||||
NO_TIME_SYNC = noch keine Uhrensynchronisation
|
||||
|
||||
@@ -68,16 +68,16 @@ ORDER_UNKNOWN = unbekannte asset order ID
|
||||
GROUP_UNKNOWN = Gruppe unbekannt
|
||||
|
||||
### Foreign Blockchain ###
|
||||
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = foreign blokchain or ElectrumX network issue
|
||||
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = fremde Blockchain oder ElectrumX Netzwerk Problem
|
||||
|
||||
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = insufficient balance on foreign blockchain
|
||||
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = unzureichend Bilanz auf fremde blockchain
|
||||
|
||||
FOREIGN_BLOCKCHAIN_TOO_SOON = too soon to broadcast foreign blockchain transaction (LockTime/median block time)
|
||||
FOREIGN_BLOCKCHAIN_TOO_SOON = zu früh um fremde Blockchain-Transaktionen zu übertragen (Sperrzeit/mittlere Blockzeit)
|
||||
|
||||
### Trade Portal ###
|
||||
ORDER_SIZE_TOO_SMALL = order amount too low
|
||||
ORDER_SIZE_TOO_SMALL = Bestellmenge zu niedrig
|
||||
|
||||
### Data ###
|
||||
FILE_NOT_FOUND = Datei nicht gefunden
|
||||
|
||||
NO_REPLY = peer did not reply with data
|
||||
NO_REPLY = Peer hat nicht mit Daten verbinden
|
||||
|
||||
83
src/main/resources/i18n/ApiError_ko.properties
Normal file
83
src/main/resources/i18n/ApiError_ko.properties
Normal file
@@ -0,0 +1,83 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# Keys are from api.ApiError enum
|
||||
|
||||
# "localeLang": "ko",
|
||||
|
||||
### Common ###
|
||||
JSON = JSON 메시지를 구문 분석하지 못했습니다.
|
||||
|
||||
INSUFFICIENT_BALANCE = 잔고 부족
|
||||
|
||||
UNAUTHORIZED = 승인되지 않은 API 호출
|
||||
|
||||
REPOSITORY_ISSUE = 리포지토리 오류
|
||||
|
||||
NON_PRODUCTION = 이 API 호출은 프로덕션 시스템에 허용되지 않습니다.
|
||||
|
||||
BLOCKCHAIN_NEEDS_SYNC = 블록체인이 먼저 동기화되어야 함
|
||||
|
||||
NO_TIME_SYNC = 아직 동기화가 없습니다.
|
||||
|
||||
### Validation ###
|
||||
INVALID_SIGNATURE = 무효 서명
|
||||
|
||||
INVALID_ADDRESS = 잘못된 주소
|
||||
|
||||
INVALID_PUBLIC_KEY = 잘못된 공개 키
|
||||
|
||||
INVALID_DATA = 잘못된 데이터
|
||||
|
||||
INVALID_NETWORK_ADDRESS = 잘못된 네트워크 주소
|
||||
|
||||
ADDRESS_UNKNOWN = 계정 주소 알 수 없음
|
||||
|
||||
INVALID_CRITERIA = 잘못된 검색 기준
|
||||
|
||||
INVALID_REFERENCE = 무효 참조
|
||||
|
||||
TRANSFORMATION_ERROR = JSON을 트랜잭션으로 변환할 수 없습니다.
|
||||
|
||||
INVALID_PRIVATE_KEY = 잘못된 개인 키
|
||||
|
||||
INVALID_HEIGHT = 잘못된 블록 높이
|
||||
|
||||
CANNOT_MINT = 계정을 만들 수 없습니다.
|
||||
|
||||
### Blocks ###
|
||||
BLOCK_UNKNOWN = 알 수 없는 블록
|
||||
|
||||
### Transactions ###
|
||||
TRANSACTION_UNKNOWN = 알 수 없는 거래
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = 공개 키를 찾을 수 없음
|
||||
|
||||
# this one is special in that caller expected to pass two additional strings, hence the two %s
|
||||
TRANSACTION_INVALID = 유효하지 않은 거래: %s (%s)
|
||||
|
||||
### Naming ###
|
||||
NAME_UNKNOWN = 이름 미상
|
||||
|
||||
### Asset ###
|
||||
INVALID_ASSET_ID = 잘못된 자산 ID
|
||||
|
||||
INVALID_ORDER_ID = 자산 주문 ID가 잘못되었습니다.
|
||||
|
||||
ORDER_UNKNOWN = 알 수 없는 자산 주문 ID
|
||||
|
||||
### Groups ###
|
||||
GROUP_UNKNOWN = 알 수 없는 그룹
|
||||
|
||||
### Foreign Blockchain ###
|
||||
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = 외부 블록체인 또는 일렉트럼X 네트워크 문제
|
||||
|
||||
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = 외부 블록체인 잔액 부족
|
||||
|
||||
FOREIGN_BLOCKCHAIN_TOO_SOON = 외부 블록체인 트랜잭션을 브로드캐스트하기에는 너무 빠릅니다(LockTime/중앙 블록 시간).
|
||||
|
||||
### Trade Portal ###
|
||||
ORDER_SIZE_TOO_SMALL = 주문량이 너무 적다
|
||||
|
||||
### Data ###
|
||||
FILE_NOT_FOUND = 파일을 찾을 수 없음
|
||||
|
||||
NO_REPLY = 피어가 허용된 시간 내에 응답하지 않음
|
||||
83
src/main/resources/i18n/ApiError_ro.properties
Normal file
83
src/main/resources/i18n/ApiError_ro.properties
Normal file
@@ -0,0 +1,83 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# Keys are from api.ApiError enum
|
||||
|
||||
# "localeLang": "ro",
|
||||
|
||||
### Comun ###
|
||||
JSON = nu s-a reusit analizarea mesajului JSON
|
||||
|
||||
INSUFFICIENT_BALANCE = fonduri insuficiente
|
||||
|
||||
UNAUTHORIZED = Solicitare API neautorizata
|
||||
|
||||
REPOSITORY_ISSUE = eroare a depozitarului
|
||||
|
||||
NON_PRODUCTION = aceasta solictare API nu este permisa pentru sistemele de productie
|
||||
|
||||
BLOCKCHAIN_NEEDS_SYNC = blockchain-ul trebuie sa se sincronizeze mai intai
|
||||
|
||||
NO_TIME_SYNC = nu exista inca o sincronizare a ceasului
|
||||
|
||||
### Validation ###
|
||||
INVALID_SIGNATURE = semnatura invalida
|
||||
|
||||
INVALID_ADDRESS = adresa invalida
|
||||
|
||||
INVALID_PUBLIC_KEY = cheie publica invalid
|
||||
|
||||
INVALID_DATA = date invalida
|
||||
|
||||
INVALID_NETWORK_ADDRESS = invalid network address
|
||||
|
||||
ADDRESS_UNKNOWN = adresa contului necunoscuta
|
||||
|
||||
INVALID_CRITERIA = criteriu de cautare invalid
|
||||
|
||||
INVALID_REFERENCE = referinta invalida
|
||||
|
||||
TRANSFORMATION_ERROR = nu s-a putut transforma JSON in tranzactie
|
||||
|
||||
INVALID_PRIVATE_KEY = invalid private key
|
||||
|
||||
INVALID_HEIGHT = dimensiunea blocului invalida
|
||||
|
||||
CANNOT_MINT = contul nu poate produce moneda
|
||||
|
||||
### Blocks ###
|
||||
BLOCK_UNKNOWN = bloc necunoscut
|
||||
|
||||
### Transactions ###
|
||||
TRANSACTION_UNKNOWN = tranzactie necunoscuta
|
||||
|
||||
PUBLIC_KEY_NOT_FOUND = nu s-a gasit cheia publica
|
||||
|
||||
# this one is special in that caller expected to pass two additional strings, hence the two %s
|
||||
TRANSACTION_INVALID = tranzactie invalida: %s (%s)
|
||||
|
||||
### Naming ###
|
||||
NAME_UNKNOWN = nume necunoscut
|
||||
|
||||
### Asset ###
|
||||
INVALID_ASSET_ID = ID active invalid
|
||||
|
||||
INVALID_ORDER_ID = ID-ul de comanda al activului invalid
|
||||
|
||||
ORDER_UNKNOWN = ID necunoscut al comenzii activului
|
||||
|
||||
### Groups ###
|
||||
GROUP_UNKNOWN = grup necunoscut
|
||||
|
||||
### Foreign Blockchain ###
|
||||
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = problema de blockchain strain sau de retea ElectrumX
|
||||
|
||||
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = sold insuficient pe blockchain strain
|
||||
|
||||
FOREIGN_BLOCKCHAIN_TOO_SOON = prea devreme pentru a difuza o tranzactie blockchain straina (LockTime/median block time)
|
||||
|
||||
### Trade Portal ###
|
||||
ORDER_SIZE_TOO_SMALL = valoarea tranzactiei este prea mica
|
||||
|
||||
### Data ###
|
||||
FILE_NOT_FOUND = nu s-a gasit fisierul
|
||||
|
||||
NO_REPLY = omologul nu a raspuns in termenul stabilit
|
||||
46
src/main/resources/i18n/SysTray_ko.properties
Normal file
46
src/main/resources/i18n/SysTray_ko.properties
Normal file
@@ -0,0 +1,46 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# SysTray pop-up menu
|
||||
|
||||
APPLYING_UPDATE_AND_RESTARTING = 자동 업데이트를 적용하고 다시 시작하는 중...
|
||||
|
||||
AUTO_UPDATE = 자동 업데이트
|
||||
|
||||
BLOCK_HEIGHT = 높이
|
||||
|
||||
BUILD_VERSION = 빌드 버전
|
||||
|
||||
CHECK_TIME_ACCURACY = 시간 정확도 점검
|
||||
|
||||
CONNECTING = 연결하는
|
||||
|
||||
CONNECTION = 연결
|
||||
|
||||
CONNECTIONS = 연결
|
||||
|
||||
CREATING_BACKUP_OF_DB_FILES = 데이터베이스 파일의 백업을 만드는 중...
|
||||
|
||||
DB_BACKUP = Database Backup
|
||||
|
||||
DB_CHECKPOINT = Database Checkpoint
|
||||
|
||||
DB_MAINTENANCE = 데이터베이스 유지 관리
|
||||
|
||||
EXIT = 종료
|
||||
|
||||
LITE_NODE = 라이트 노드
|
||||
|
||||
MINTING_DISABLED = 민팅중이 아님
|
||||
|
||||
MINTING_ENABLED = \u2714 민팅
|
||||
|
||||
OPEN_UI = UI 열기
|
||||
|
||||
PERFORMING_DB_CHECKPOINT = 커밋되지 않은 데이터베이스 변경 내용을 저장하는 중...
|
||||
|
||||
PERFORMING_DB_MAINTENANCE = 예약된 유지 관리 수행 중...
|
||||
|
||||
SYNCHRONIZE_CLOCK = 시간 동기화
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = 동기화중
|
||||
|
||||
SYNCHRONIZING_CLOCK = 시간 동기화
|
||||
46
src/main/resources/i18n/SysTray_ro.properties
Normal file
46
src/main/resources/i18n/SysTray_ro.properties
Normal file
@@ -0,0 +1,46 @@
|
||||
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
|
||||
# SysTray pop-up menu
|
||||
|
||||
APPLYING_UPDATE_AND_RESTARTING = Aplicarea actualizarii automate si repornire...
|
||||
|
||||
AUTO_UPDATE = Actualizare automata
|
||||
|
||||
BLOCK_HEIGHT = dimensiune
|
||||
|
||||
BUILD_VERSION = versiunea compilatiei
|
||||
|
||||
CHECK_TIME_ACCURACY = verificare exactitate ora
|
||||
|
||||
CONNECTING = Se conecteaza
|
||||
|
||||
CONNECTION = conexiune
|
||||
|
||||
CONNECTIONS = conexiuni
|
||||
|
||||
CREATING_BACKUP_OF_DB_FILES = Se creaza copia bazei de date
|
||||
|
||||
DB_BACKUP = Copie baza de date
|
||||
|
||||
DB_CHECKPOINT = Punct de control al bazei de date
|
||||
|
||||
DB_MAINTENANCE = Database Maintenance
|
||||
|
||||
EXIT = iesire
|
||||
|
||||
LITE_NODE = Nod Lite
|
||||
|
||||
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<74>
|
||||
|
||||
SYNCHRONIZE_CLOCK = Sincronizare ceas
|
||||
|
||||
SYNCHRONIZING_BLOCKCHAIN = Sincronizare
|
||||
|
||||
SYNCHRONIZING_CLOCK = Se sincronizeaza ceasul
|
||||
@@ -25,7 +25,7 @@ DB_CHECKPOINT = Databaskontrollpunkt
|
||||
|
||||
DB_MAINTENANCE = Databasunderhåll
|
||||
|
||||
EXIT = Utgång
|
||||
EXIT = Avsluta
|
||||
|
||||
MINTING_DISABLED = Präglar INTE
|
||||
|
||||
|
||||
195
src/main/resources/i18n/TransactionValidity_ko.properties
Normal file
195
src/main/resources/i18n/TransactionValidity_ko.properties
Normal file
@@ -0,0 +1,195 @@
|
||||
#
|
||||
|
||||
ACCOUNT_ALREADY_EXISTS = 계정이 이미 존재합니다.
|
||||
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = 계정이 보상을 공유할 수 없습니다.
|
||||
|
||||
ADDRESS_ABOVE_RATE_LIMIT = 주소가 지정된 속도 제한에 도달했습니다.
|
||||
|
||||
ADDRESS_BLOCKED = 이 주소는 차단되었습니다.
|
||||
|
||||
ALREADY_GROUP_ADMIN = 이미 그룹 관리자
|
||||
|
||||
ALREADY_GROUP_MEMBER = 이미 그룹 맴버
|
||||
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = 이미 그 옵션에 투표했다.
|
||||
|
||||
ASSET_ALREADY_EXISTS = 자산이 이미 있습니다.
|
||||
|
||||
ASSET_DOES_NOT_EXIST = 자산이 존재하지 않습니다.
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = 자산이 AT의 자산과 일치하지 않습니다.
|
||||
|
||||
ASSET_NOT_SPENDABLE = 자산을 사용할 수 없습니다.
|
||||
|
||||
AT_ALREADY_EXISTS = AT가 이미 있습니다.
|
||||
|
||||
AT_IS_FINISHED = AT가 완료되었습니다.
|
||||
|
||||
AT_UNKNOWN = 알 수 없는 AT
|
||||
|
||||
BAN_EXISTS = 금지가 이미 있습니다.
|
||||
|
||||
BAN_UNKNOWN = 금지 알 수 없음
|
||||
|
||||
BANNED_FROM_GROUP = 그룹에서 금지
|
||||
|
||||
BUYER_ALREADY_OWNER = 구매자는 이미 소유자입니다
|
||||
|
||||
CLOCK_NOT_SYNCED = 동기화되지 않은 시간
|
||||
|
||||
DUPLICATE_MESSAGE = 주소가 중복 메시지를 보냈습니다.
|
||||
|
||||
DUPLICATE_OPTION = 중복 옵션
|
||||
|
||||
GROUP_ALREADY_EXISTS = 그룹이 이미 존재합니다
|
||||
|
||||
GROUP_APPROVAL_DECIDED = 그룹 승인이 이미 결정되었습니다.
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = 그룹 승인이 필요하지 않음
|
||||
|
||||
GROUP_DOES_NOT_EXIST = 그룹이 존재하지 않습니다
|
||||
|
||||
GROUP_ID_MISMATCH = 그룹 ID 불일치
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = 그룹 소유자는 그룹을 나갈 수 없습니다
|
||||
|
||||
HAVE_EQUALS_WANT = 소유 자산은 원하는 자산과 동일합니다.
|
||||
|
||||
INCORRECT_NONCE = 잘못된 PoW nonce
|
||||
|
||||
INSUFFICIENT_FEE = 부족한 수수료
|
||||
|
||||
INVALID_ADDRESS = 잘못된 주소
|
||||
|
||||
INVALID_AMOUNT = 유효하지 않은 금액
|
||||
|
||||
INVALID_ASSET_OWNER = 잘못된 자산 소유자
|
||||
|
||||
INVALID_AT_TRANSACTION = 유효하지 않은 AT 거래
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = 잘못된 AT '유형' 길이
|
||||
|
||||
INVALID_BUT_OK = 유효하지 않지만 OK
|
||||
|
||||
INVALID_CREATION_BYTES = 잘못된 생성 바이트
|
||||
|
||||
INVALID_DATA_LENGTH = 잘못된 데이터 길이
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = 잘못된 설명 길이
|
||||
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = 잘못된 그룹 승인 임계값
|
||||
|
||||
INVALID_GROUP_BLOCK_DELAY = 잘못된 그룹 승인 차단 지연
|
||||
|
||||
INVALID_GROUP_ID = 잘못된 그룹 ID
|
||||
|
||||
INVALID_GROUP_OWNER = 잘못된 그룹 소유자
|
||||
|
||||
INVALID_LIFETIME = 유효하지 않은 수명
|
||||
|
||||
INVALID_NAME_LENGTH = 잘못된 이름 길이
|
||||
|
||||
INVALID_NAME_OWNER = 잘못된 이름 소유자
|
||||
|
||||
INVALID_OPTION_LENGTH = 잘못된 옵션 길이
|
||||
|
||||
INVALID_OPTIONS_COUNT = 잘못된 옵션 수
|
||||
|
||||
INVALID_ORDER_CREATOR = 잘못된 주문 생성자
|
||||
|
||||
INVALID_PAYMENTS_COUNT = 유효하지 않은 지불 수
|
||||
|
||||
INVALID_PUBLIC_KEY = 잘못된 공개 키
|
||||
|
||||
INVALID_QUANTITY = 유효하지 않은 수량
|
||||
|
||||
INVALID_REFERENCE = 잘못된 참조
|
||||
|
||||
INVALID_RETURN = 무효 반환
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = 잘못된 보상 공유 비율
|
||||
|
||||
INVALID_SELLER = 무효 판매자
|
||||
|
||||
INVALID_TAGS_LENGTH = invalid 'tags' length
|
||||
|
||||
INVALID_TIMESTAMP_SIGNATURE = 유효하지 않은 타임스탬프 서명
|
||||
|
||||
INVALID_TX_GROUP_ID = 잘못된 트랜잭션 그룹 ID
|
||||
|
||||
INVALID_VALUE_LENGTH = 잘못된 '값' 길이
|
||||
|
||||
INVITE_UNKNOWN = 알 수 없는 그룹 초대
|
||||
|
||||
JOIN_REQUEST_EXISTS = 그룹 가입 요청이 이미 있습니다.
|
||||
|
||||
MAXIMUM_REWARD_SHARES = 이미 이 계정에 대한 최대 보상 공유 수에 도달했습니다.t
|
||||
|
||||
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 = 유니코드 '정규화된' 형식이 아닌 이름
|
||||
|
||||
NEGATIVE_AMOUNT = 유효하지 않은/음수 금액
|
||||
|
||||
NEGATIVE_FEE = 무효/음수 수수료
|
||||
|
||||
NEGATIVE_PRICE = 유효하지 않은/음수 가격
|
||||
|
||||
NO_BALANCE = 잔액 불충분
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = 노드의 블록체인이 현재 사용 중입니다.
|
||||
|
||||
NO_FLAG_PERMISSION = 계정에 해당 권한이 없습니다
|
||||
|
||||
NOT_GROUP_ADMIN = 계정은 그룹 관리자가 아닙니다.
|
||||
|
||||
NOT_GROUP_MEMBER = 계정이 그룹 구성원이 아닙니다.
|
||||
|
||||
NOT_MINTING_ACCOUNT = 계정은 발행할 수 없습니다
|
||||
|
||||
NOT_YET_RELEASED = 아직 출시되지 않은 기능
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = 아직 출시되지 않은 기능
|
||||
|
||||
ORDER_DOES_NOT_EXIST = 자산 거래 주문이 존재하지 않습니다
|
||||
|
||||
POLL_ALREADY_EXISTS = 설문조사가 이미 존재합니다
|
||||
|
||||
POLL_DOES_NOT_EXIST = 설문조사가 존재하지 않습니다
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = 투표 옵션이 존재하지 않습니다
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = 공개 키 알 수 없음
|
||||
|
||||
REWARD_SHARE_UNKNOWN = 알 수 없는 보상 공유
|
||||
|
||||
SELF_SHARE_EXISTS = 자체 공유(보상 공유)가 이미 존재합니다.
|
||||
|
||||
TIMESTAMP_TOO_NEW = 타임스탬프가 너무 새롭습니다.
|
||||
|
||||
TIMESTAMP_TOO_OLD = 너무 오래된 타임스탬프
|
||||
|
||||
TOO_MANY_UNCONFIRMED = 계정에 보류 중인 확인되지 않은 거래가 너무 많습니다.
|
||||
|
||||
TRANSACTION_ALREADY_CONFIRMED = 거래가 이미 확인되었습니다
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = 거래가 이미 존재합니다
|
||||
|
||||
TRANSACTION_UNKNOWN = 알 수 없는 거래
|
||||
|
||||
TX_GROUP_ID_MISMATCH = 트랜잭션의 그룹 ID가 일치하지 않습니다
|
||||
195
src/main/resources/i18n/TransactionValidity_ro.properties
Normal file
195
src/main/resources/i18n/TransactionValidity_ro.properties
Normal file
@@ -0,0 +1,195 @@
|
||||
#
|
||||
|
||||
ACCOUNT_ALREADY_EXISTS = contul exista deja
|
||||
|
||||
ACCOUNT_CANNOT_REWARD_SHARE = contul nu poate genera reward-share
|
||||
|
||||
ADDRESS_ABOVE_RATE_LIMIT = adresa a atins limita specificata
|
||||
|
||||
ADDRESS_BLOCKED = aceasta adresa este blocata
|
||||
|
||||
ALREADY_GROUP_ADMIN = sunteti deja admin
|
||||
|
||||
ALREADY_GROUP_MEMBER = sunteti deja membru
|
||||
|
||||
ALREADY_VOTED_FOR_THAT_OPTION = deja ati votat pentru aceasta optiune
|
||||
|
||||
ASSET_ALREADY_EXISTS = activul deja exista
|
||||
|
||||
ASSET_DOES_NOT_EXIST = activul un exista
|
||||
|
||||
ASSET_DOES_NOT_MATCH_AT = activul nu se potriveste cu activul TA
|
||||
|
||||
ASSET_NOT_SPENDABLE = activul nu poate fi utilizat
|
||||
|
||||
AT_ALREADY_EXISTS = TA exista deja
|
||||
|
||||
AT_IS_FINISHED = TA s-a terminat
|
||||
|
||||
AT_UNKNOWN = TA necunoscuta
|
||||
|
||||
BAN_EXISTS = ban-ul este deja folosit
|
||||
|
||||
BAN_UNKNOWN = ban necunoscut
|
||||
|
||||
BANNED_FROM_GROUP = accesul la grup a fost blocat
|
||||
|
||||
BUYER_ALREADY_OWNER = cumparatorul este deja detinator
|
||||
|
||||
CLOCK_NOT_SYNCED = ceasul nu este sincronizat
|
||||
|
||||
DUPLICATE_MESSAGE = adresa a trimis mesaje duplicate
|
||||
|
||||
DUPLICATE_OPTION = optiune duplicata
|
||||
|
||||
GROUP_ALREADY_EXISTS = grupul deja exista
|
||||
|
||||
GROUP_APPROVAL_DECIDED = aprobarea grupului a fost deja decisa
|
||||
|
||||
GROUP_APPROVAL_NOT_REQUIRED = aprobarea grupului nu este solicitata
|
||||
|
||||
GROUP_DOES_NOT_EXIST = grupul nu exista
|
||||
|
||||
GROUP_ID_MISMATCH = ID-ul grupului incorect
|
||||
|
||||
GROUP_OWNER_CANNOT_LEAVE = proprietarul grupului nu poate parasi grupul
|
||||
|
||||
HAVE_EQUALS_WANT = a avea un obiect este acelasi lucru cu a vrea un obiect
|
||||
|
||||
INCORRECT_NONCE = numar PoW incorect
|
||||
|
||||
INSUFFICIENT_FEE = taxa insuficienta
|
||||
|
||||
INVALID_ADDRESS = adresa invalida
|
||||
|
||||
INVALID_AMOUNT = suma invalida
|
||||
|
||||
INVALID_ASSET_OWNER = propietar al activului invalid
|
||||
|
||||
INVALID_AT_TRANSACTION = tranzactie automata invalida
|
||||
|
||||
INVALID_AT_TYPE_LENGTH = TA invalida 'tip' lungime
|
||||
|
||||
INVALID_BUT_OK = invalid dar OK
|
||||
|
||||
INVALID_CREATION_BYTES = octeti de creatie invalizi
|
||||
|
||||
INVALID_DATA_LENGTH = lungimea datelor invalida
|
||||
|
||||
INVALID_DESCRIPTION_LENGTH = lungimea descrierii invalida
|
||||
|
||||
INVALID_GROUP_APPROVAL_THRESHOLD = prag de aprobare a grupului invalid
|
||||
|
||||
INVALID_GROUP_BLOCK_DELAY = intarziere invalida a blocului de aprobare a grupului
|
||||
|
||||
INVALID_GROUP_ID = ID de grup invalid
|
||||
|
||||
INVALID_GROUP_OWNER = proprietar de grup invalid
|
||||
|
||||
INVALID_LIFETIME = durata de viata invalida
|
||||
|
||||
INVALID_NAME_LENGTH = lungimea numelui invalida
|
||||
|
||||
INVALID_NAME_OWNER = numele proprietarului invalid
|
||||
|
||||
INVALID_OPTION_LENGTH = lungimea optiunii invalida
|
||||
|
||||
INVALID_OPTIONS_COUNT = contor de optiuni invalid
|
||||
|
||||
INVALID_ORDER_CREATOR = creator de ordine invalid
|
||||
|
||||
INVALID_PAYMENTS_COUNT = contor de plati invalid
|
||||
|
||||
INVALID_PUBLIC_KEY = cheie publica invalida
|
||||
|
||||
INVALID_QUANTITY = cantitate invalida
|
||||
|
||||
INVALID_REFERENCE = referinta invalida
|
||||
|
||||
INVALID_RETURN = returnare invalida
|
||||
|
||||
INVALID_REWARD_SHARE_PERCENT = procentaj al cotei de recompensa invalid
|
||||
|
||||
INVALID_SELLER = vanzator invalid
|
||||
|
||||
INVALID_TAGS_LENGTH = lungime a tagurilor invalida
|
||||
|
||||
INVALID_TIMESTAMP_SIGNATURE = semnatura timestamp invalida
|
||||
|
||||
INVALID_TX_GROUP_ID = ID-ul grupului de tranzactii invalid
|
||||
|
||||
INVALID_VALUE_LENGTH = lungimea "valorii "invalida
|
||||
|
||||
INVITE_UNKNOWN = invitatie de grup invalida
|
||||
|
||||
JOIN_REQUEST_EXISTS = cererea de aderare la grup exista deja
|
||||
|
||||
MAXIMUM_REWARD_SHARES = ati ajuns deja la numarul maxim de cote de recompensa pentru acest cont
|
||||
|
||||
MISSING_CREATOR = creator lipsa
|
||||
|
||||
MULTIPLE_NAMES_FORBIDDEN = este interzisa folosirea mai multor nume inregistrate pe cont
|
||||
|
||||
NAME_ALREADY_FOR_SALE = numele este deja de vanzare
|
||||
|
||||
NAME_ALREADY_REGISTERED = nume deja inregistrat
|
||||
|
||||
NAME_BLOCKED = numele este blocat
|
||||
|
||||
NAME_DOES_NOT_EXIST = numele nu exista
|
||||
|
||||
NAME_NOT_FOR_SALE = numele nu este de vanzare
|
||||
|
||||
NAME_NOT_NORMALIZED = numele nu este in forma "normalizata" Unicode
|
||||
|
||||
NEGATIVE_AMOUNT = suma invalida/negativa
|
||||
|
||||
NEGATIVE_FEE = taxa invalida/negativa
|
||||
|
||||
NEGATIVE_PRICE = pret invalid/negativ
|
||||
|
||||
NO_BALANCE = fonduri insuficiente
|
||||
|
||||
NO_BLOCKCHAIN_LOCK = nodul blochain-ului este momentan ocupat
|
||||
|
||||
NO_FLAG_PERMISSION = contul nu are aceasta permisiune
|
||||
|
||||
NOT_GROUP_ADMIN = contul nu este un administrator de grup
|
||||
|
||||
NOT_GROUP_MEMBER = contul nu este un membru al grupului
|
||||
|
||||
NOT_MINTING_ACCOUNT = contul nu poate genera moneda Qort
|
||||
|
||||
NOT_YET_RELEASED = caracteristica nu este inca disponibila
|
||||
|
||||
OK = OK
|
||||
|
||||
ORDER_ALREADY_CLOSED = ordinul de tranzactionare a activului este deja inchis
|
||||
|
||||
ORDER_DOES_NOT_EXIST = ordinul de comercializare a activului nu exista
|
||||
|
||||
POLL_ALREADY_EXISTS = sondajul exista deja
|
||||
|
||||
POLL_DOES_NOT_EXIST = sondajul nu exista
|
||||
|
||||
POLL_OPTION_DOES_NOT_EXIST = optiunea de sondaj nu exista
|
||||
|
||||
PUBLIC_KEY_UNKNOWN = cheie publica necunoscuta
|
||||
|
||||
REWARD_SHARE_UNKNOWN = cheie de cota de recompensa necunoscuta
|
||||
|
||||
SELF_SHARE_EXISTS = cota personala (cota de recompensa) exista deja
|
||||
|
||||
TIMESTAMP_TOO_NEW = timestamp prea nou
|
||||
|
||||
TIMESTAMP_TOO_OLD = timestamp prea vechi
|
||||
|
||||
TOO_MANY_UNCONFIRMED = contul are prea multe tranzactii neconfirmate in asteptare
|
||||
|
||||
TRANZACTIE_DEJA_CONFIRMATA = tranzactia a fost deja confirmata
|
||||
|
||||
TRANSACTION_ALREADY_EXISTS = tranzactia exista deja
|
||||
|
||||
TRANSACTION_UNKNOWN = tranzactie necunoscuta
|
||||
|
||||
TX_GROUP_ID_MISMATCH = ID-ul de grup al tranzactiei nu se potriveste
|
||||
57
src/main/resources/proto/zcash/compact_formats.proto
Normal file
57
src/main/resources/proto/zcash/compact_formats.proto
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2019-2020 The Zcash developers
|
||||
// Copyright (c) 2019-2021 Pirate Chain developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||
|
||||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.rpc;
|
||||
option go_package = "lightwalletd/walletrpc";
|
||||
option swift_prefix = "";
|
||||
// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value.
|
||||
// bytes fields of hashes are in canonical little-endian format.
|
||||
|
||||
// CompactBlock is a packaging of ONLY the data from a block that's needed to:
|
||||
// 1. Detect a payment to your shielded Sapling address
|
||||
// 2. Detect a spend of your shielded Sapling notes
|
||||
// 3. Update your witnesses to generate new Sapling spend proofs.
|
||||
message CompactBlock {
|
||||
uint32 protoVersion = 1; // the version of this wire format, for storage
|
||||
uint64 height = 2; // the height of this block
|
||||
bytes hash = 3; // the ID (hash) of this block, same as in block explorers
|
||||
bytes prevHash = 4; // the ID (hash) of this block's predecessor
|
||||
uint32 time = 5; // Unix epoch time when the block was mined
|
||||
bytes header = 6; // (hash, prevHash, and time) OR (full header)
|
||||
repeated CompactTx vtx = 7; // zero or more compact transactions from this block
|
||||
}
|
||||
|
||||
// CompactTx contains the minimum information for a wallet to know if this transaction
|
||||
// is relevant to it (either pays to it or spends from it) via shielded elements
|
||||
// only. This message will not encode a transparent-to-transparent transaction.
|
||||
message CompactTx {
|
||||
uint64 index = 1; // the index within the full block
|
||||
bytes hash = 2; // the ID (hash) of this transaction, same as in block explorers
|
||||
|
||||
// The transaction fee: present if server can provide. In the case of a
|
||||
// stateless server and a transaction with transparent inputs, this will be
|
||||
// unset because the calculation requires reference to prior transactions.
|
||||
// in a pure-Sapling context, the fee will be calculable as:
|
||||
// valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut))
|
||||
uint32 fee = 3;
|
||||
|
||||
repeated CompactSpend spends = 4; // inputs
|
||||
repeated CompactOutput outputs = 5; // outputs
|
||||
}
|
||||
|
||||
// CompactSpend is a Sapling Spend Description as described in 7.3 of the Zcash
|
||||
// protocol specification.
|
||||
message CompactSpend {
|
||||
bytes nf = 1; // nullifier (see the Zcash protocol specification)
|
||||
}
|
||||
|
||||
// output is a Sapling Output Description as described in section 7.4 of the
|
||||
// Zcash protocol spec. Total size is 948.
|
||||
message CompactOutput {
|
||||
bytes cmu = 1; // note commitment u-coordinate
|
||||
bytes epk = 2; // ephemeral public key
|
||||
bytes ciphertext = 3; // ciphertext and zkproof
|
||||
}
|
||||
117
src/main/resources/proto/zcash/darkside.proto
Normal file
117
src/main/resources/proto/zcash/darkside.proto
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2019-2020 The Zcash developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||
|
||||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.rpc;
|
||||
option go_package = "lightwalletd/walletrpc";
|
||||
option swift_prefix = "";
|
||||
import "service.proto";
|
||||
|
||||
message DarksideMetaState {
|
||||
int32 saplingActivation = 1;
|
||||
string branchID = 2;
|
||||
string chainName = 3;
|
||||
}
|
||||
|
||||
// A block is a hex-encoded string.
|
||||
message DarksideBlock {
|
||||
string block = 1;
|
||||
}
|
||||
|
||||
// DarksideBlocksURL is typically something like:
|
||||
// https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt
|
||||
message DarksideBlocksURL {
|
||||
string url = 1;
|
||||
}
|
||||
|
||||
// DarksideTransactionsURL refers to an HTTP source that contains a list
|
||||
// of hex-encoded transactions, one per line, that are to be associated
|
||||
// with the given height (fake-mined into the block at that height)
|
||||
message DarksideTransactionsURL {
|
||||
int32 height = 1;
|
||||
string url = 2;
|
||||
}
|
||||
|
||||
message DarksideHeight {
|
||||
int32 height = 1;
|
||||
}
|
||||
|
||||
message DarksideEmptyBlocks {
|
||||
int32 height = 1;
|
||||
int32 nonce = 2;
|
||||
int32 count = 3;
|
||||
}
|
||||
|
||||
// Darksidewalletd maintains two staging areas, blocks and transactions. The
|
||||
// Stage*() gRPCs add items to the staging area; ApplyStaged() "applies" everything
|
||||
// in the staging area to the working (operational) state that the mock zcashd
|
||||
// serves; transactions are placed into their corresponding blocks (by height).
|
||||
service DarksideStreamer {
|
||||
// Reset reverts all darksidewalletd state (active block range, latest height,
|
||||
// staged blocks and transactions) and lightwalletd state (cache) to empty,
|
||||
// the same as the initial state. This occurs synchronously and instantaneously;
|
||||
// no reorg happens in lightwalletd. This is good to do before each independent
|
||||
// test so that no state leaks from one test to another.
|
||||
// Also sets (some of) the values returned by GetLightdInfo(). The Sapling
|
||||
// activation height specified here must be where the block range starts.
|
||||
rpc Reset(DarksideMetaState) returns (Empty) {}
|
||||
|
||||
// StageBlocksStream accepts a list of blocks and saves them into the blocks
|
||||
// staging area until ApplyStaged() is called; there is no immediate effect on
|
||||
// the mock zcashd. Blocks are hex-encoded. Order is important, see ApplyStaged.
|
||||
rpc StageBlocksStream(stream DarksideBlock) returns (Empty) {}
|
||||
|
||||
// StageBlocks is the same as StageBlocksStream() except the blocks are fetched
|
||||
// from the given URL. Blocks are one per line, hex-encoded (not JSON).
|
||||
rpc StageBlocks(DarksideBlocksURL) returns (Empty) {}
|
||||
|
||||
// StageBlocksCreate is like the previous two, except it creates 'count'
|
||||
// empty blocks at consecutive heights starting at height 'height'. The
|
||||
// 'nonce' is part of the header, so it contributes to the block hash; this
|
||||
// lets you create identical blocks (same transactions and height), but with
|
||||
// different hashes.
|
||||
rpc StageBlocksCreate(DarksideEmptyBlocks) returns (Empty) {}
|
||||
|
||||
// StageTransactionsStream stores the given transaction-height pairs in the
|
||||
// staging area until ApplyStaged() is called. Note that these transactions
|
||||
// are not returned by the production GetTransaction() gRPC until they
|
||||
// appear in a "mined" block (contained in the active blockchain presented
|
||||
// by the mock zcashd).
|
||||
rpc StageTransactionsStream(stream RawTransaction) returns (Empty) {}
|
||||
|
||||
// StageTransactions is the same except the transactions are fetched from
|
||||
// the given url. They are all staged into the block at the given height.
|
||||
// Staging transactions to different heights requires multiple calls.
|
||||
rpc StageTransactions(DarksideTransactionsURL) returns (Empty) {}
|
||||
|
||||
// ApplyStaged iterates the list of blocks that were staged by the
|
||||
// StageBlocks*() gRPCs, in the order they were staged, and "merges" each
|
||||
// into the active, working blocks list that the mock zcashd is presenting
|
||||
// to lightwalletd. Even as each block is applied, the active list can't
|
||||
// have gaps; if the active block range is 1000-1006, and the staged block
|
||||
// range is 1003-1004, the resulting range is 1000-1004, with 1000-1002
|
||||
// unchanged, blocks 1003-1004 from the new range, and 1005-1006 dropped.
|
||||
//
|
||||
// After merging all blocks, ApplyStaged() appends staged transactions (in
|
||||
// the order received) into each one's corresponding (by height) block
|
||||
// The staging area is then cleared.
|
||||
//
|
||||
// The argument specifies the latest block height that mock zcashd reports
|
||||
// (i.e. what's returned by GetLatestBlock). Note that ApplyStaged() can
|
||||
// also be used to simply advance the latest block height presented by mock
|
||||
// zcashd. That is, there doesn't need to be anything in the staging area.
|
||||
rpc ApplyStaged(DarksideHeight) returns (Empty) {}
|
||||
|
||||
// Calls to the production gRPC SendTransaction() store the transaction in
|
||||
// a separate area (not the staging area); this method returns all transactions
|
||||
// in this separate area, which is then cleared. The height returned
|
||||
// with each transaction is -1 (invalid) since these transactions haven't
|
||||
// been mined yet. The intention is that the transactions returned here can
|
||||
// then, for example, be given to StageTransactions() to get them "mined"
|
||||
// into a specified block on the next ApplyStaged().
|
||||
rpc GetIncomingTransactions(Empty) returns (stream RawTransaction) {}
|
||||
|
||||
// Clear the incoming transaction pool.
|
||||
rpc ClearIncomingTransactions(Empty) returns (Empty) {}
|
||||
}
|
||||
181
src/main/resources/proto/zcash/service.proto
Normal file
181
src/main/resources/proto/zcash/service.proto
Normal file
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) 2019-2020 The Zcash developers
|
||||
// Copyright (c) 2019-2021 Pirate Chain developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||
|
||||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.rpc;
|
||||
option go_package = "lightwalletd/walletrpc";
|
||||
option swift_prefix = "";
|
||||
import "compact_formats.proto";
|
||||
|
||||
// A BlockID message contains identifiers to select a block: a height or a
|
||||
// hash. Specification by hash is not implemented, but may be in the future.
|
||||
message BlockID {
|
||||
uint64 height = 1;
|
||||
bytes hash = 2;
|
||||
}
|
||||
|
||||
// BlockRange specifies a series of blocks from start to end inclusive.
|
||||
// Both BlockIDs must be heights; specification by hash is not yet supported.
|
||||
message BlockRange {
|
||||
BlockID start = 1;
|
||||
BlockID end = 2;
|
||||
}
|
||||
|
||||
// A TxFilter contains the information needed to identify a particular
|
||||
// transaction: either a block and an index, or a direct transaction hash.
|
||||
// Currently, only specification by hash is supported.
|
||||
message TxFilter {
|
||||
BlockID block = 1; // block identifier, height or hash
|
||||
uint64 index = 2; // index within the block
|
||||
bytes hash = 3; // transaction ID (hash, txid)
|
||||
}
|
||||
|
||||
// RawTransaction contains the complete transaction data. It also optionally includes
|
||||
// the block height in which the transaction was included.
|
||||
message RawTransaction {
|
||||
bytes data = 1; // exact data returned by Zcash 'getrawtransaction'
|
||||
uint64 height = 2; // height that the transaction was mined (or -1)
|
||||
}
|
||||
|
||||
// A SendResponse encodes an error code and a string. It is currently used
|
||||
// only by SendTransaction(). If error code is zero, the operation was
|
||||
// successful; if non-zero, it and the message specify the failure.
|
||||
message SendResponse {
|
||||
int32 errorCode = 1;
|
||||
string errorMessage = 2;
|
||||
}
|
||||
|
||||
// Chainspec is a placeholder to allow specification of a particular chain fork.
|
||||
message ChainSpec {}
|
||||
|
||||
// Empty is for gRPCs that take no arguments, currently only GetLightdInfo.
|
||||
message Empty {}
|
||||
|
||||
// LightdInfo returns various information about this lightwalletd instance
|
||||
// and the state of the blockchain.
|
||||
message LightdInfo {
|
||||
string version = 1;
|
||||
string vendor = 2;
|
||||
bool taddrSupport = 3; // true
|
||||
string chainName = 4; // either "main" or "test"
|
||||
uint64 saplingActivationHeight = 5; // depends on mainnet or testnet
|
||||
string consensusBranchId = 6; // protocol identifier, see consensus/upgrades.cpp
|
||||
uint64 blockHeight = 7; // latest block on the best chain
|
||||
string gitCommit = 8;
|
||||
string branch = 9;
|
||||
string buildDate = 10;
|
||||
string buildUser = 11;
|
||||
uint64 estimatedHeight = 12; // less than tip height if pirated is syncing
|
||||
string piratedBuild = 13; // example: "v4.1.1-877212414"
|
||||
string piratedSubversion = 14; // example: "/MagicBean:4.1.1/"
|
||||
}
|
||||
|
||||
// TransparentAddressBlockFilter restricts the results to the given address
|
||||
// or block range.
|
||||
message TransparentAddressBlockFilter {
|
||||
string address = 1; // t-address
|
||||
BlockRange range = 2; // start, end heights
|
||||
}
|
||||
|
||||
// Duration is currently used only for testing, so that the Ping rpc
|
||||
// can simulate a delay, to create many simultaneous connections. Units
|
||||
// are microseconds.
|
||||
message Duration {
|
||||
int64 intervalUs = 1;
|
||||
}
|
||||
|
||||
// PingResponse is used to indicate concurrency, how many Ping rpcs
|
||||
// are executing upon entry and upon exit (after the delay).
|
||||
// This rpc is used for testing only.
|
||||
message PingResponse {
|
||||
int64 entry = 1;
|
||||
int64 exit = 2;
|
||||
}
|
||||
|
||||
message Address {
|
||||
string address = 1;
|
||||
}
|
||||
message AddressList {
|
||||
repeated string addresses = 1;
|
||||
}
|
||||
message Balance {
|
||||
int64 valueZat = 1;
|
||||
}
|
||||
|
||||
message Exclude {
|
||||
repeated bytes txid = 1;
|
||||
}
|
||||
|
||||
// The TreeState is derived from the Zcash z_gettreestate rpc.
|
||||
message TreeState {
|
||||
string network = 1; // "main" or "test"
|
||||
uint64 height = 2;
|
||||
string hash = 3; // block id
|
||||
uint32 time = 4; // Unix epoch time when the block was mined
|
||||
string tree = 5; // sapling commitment tree state
|
||||
}
|
||||
|
||||
// Results are sorted by height, which makes it easy to issue another
|
||||
// request that picks up from where the previous left off.
|
||||
message GetAddressUtxosArg {
|
||||
repeated string addresses = 1;
|
||||
uint64 startHeight = 2;
|
||||
uint32 maxEntries = 3; // zero means unlimited
|
||||
}
|
||||
message GetAddressUtxosReply {
|
||||
string address = 6;
|
||||
bytes txid = 1;
|
||||
int32 index = 2;
|
||||
bytes script = 3;
|
||||
int64 valueZat = 4;
|
||||
uint64 height = 5;
|
||||
}
|
||||
message GetAddressUtxosReplyList {
|
||||
repeated GetAddressUtxosReply addressUtxos = 1;
|
||||
}
|
||||
|
||||
service CompactTxStreamer {
|
||||
// Return the height of the tip of the best chain
|
||||
rpc GetLatestBlock(ChainSpec) returns (BlockID) {}
|
||||
// Return the compact block corresponding to the given block identifier
|
||||
rpc GetBlock(BlockID) returns (CompactBlock) {}
|
||||
// Return a list of consecutive compact blocks
|
||||
rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {}
|
||||
|
||||
// Return the requested full (not compact) transaction (as from pirated)
|
||||
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
|
||||
// Submit the given transaction to the Zcash network
|
||||
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
|
||||
|
||||
// Return the txids corresponding to the given t-address within the given block range
|
||||
rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
|
||||
rpc GetTaddressBalance(AddressList) returns (Balance) {}
|
||||
rpc GetTaddressBalanceStream(stream Address) returns (Balance) {}
|
||||
|
||||
// Return the compact transactions currently in the mempool; the results
|
||||
// can be a few seconds out of date. If the Exclude list is empty, return
|
||||
// all transactions; otherwise return all *except* those in the Exclude list
|
||||
// (if any); this allows the client to avoid receiving transactions that it
|
||||
// already has (from an earlier call to this rpc). The transaction IDs in the
|
||||
// Exclude list can be shortened to any number of bytes to make the request
|
||||
// more bandwidth-efficient; if two or more transactions in the mempool
|
||||
// match a shortened txid, they are all sent (none is excluded). Transactions
|
||||
// in the exclude list that don't exist in the mempool are ignored.
|
||||
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
|
||||
|
||||
// GetTreeState returns the note commitment tree state corresponding to the given block.
|
||||
// See section 3.7 of the Zcash protocol specification. It returns several other useful
|
||||
// values also (even though they can be obtained using GetBlock).
|
||||
// The block can be specified by either height or hash.
|
||||
rpc GetTreeState(BlockID) returns (TreeState) {}
|
||||
|
||||
rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {}
|
||||
rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {}
|
||||
|
||||
// Return information about this lightwalletd instance and the blockchain
|
||||
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
|
||||
// Testing-only, requires lightwalletd --ping-very-insecure (do not enable in production)
|
||||
rpc Ping(Duration) returns (PingResponse) {}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import org.qortal.test.common.Common;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
import org.qortal.utils.BlockArchiveUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.Triple;
|
||||
@@ -123,8 +124,8 @@ public class BlockArchiveTests extends Common {
|
||||
|
||||
// Read block 2 from the archive
|
||||
BlockArchiveReader reader = BlockArchiveReader.getInstance();
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> block2Info = reader.fetchBlockAtHeight(2);
|
||||
BlockData block2ArchiveData = block2Info.getA();
|
||||
BlockTransformation block2Info = reader.fetchBlockAtHeight(2);
|
||||
BlockData block2ArchiveData = block2Info.getBlockData();
|
||||
|
||||
// Read block 2 from the repository
|
||||
BlockData block2RepositoryData = repository.getBlockRepository().fromHeight(2);
|
||||
@@ -137,8 +138,8 @@ public class BlockArchiveTests extends Common {
|
||||
assertEquals(1, block2ArchiveData.getOnlineAccountsCount());
|
||||
|
||||
// Read block 900 from the archive
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> block900Info = reader.fetchBlockAtHeight(900);
|
||||
BlockData block900ArchiveData = block900Info.getA();
|
||||
BlockTransformation block900Info = reader.fetchBlockAtHeight(900);
|
||||
BlockData block900ArchiveData = block900Info.getBlockData();
|
||||
|
||||
// Read block 900 from the repository
|
||||
BlockData block900RepositoryData = repository.getBlockRepository().fromHeight(900);
|
||||
@@ -200,10 +201,10 @@ public class BlockArchiveTests extends Common {
|
||||
|
||||
// Read a block from the archive
|
||||
BlockArchiveReader reader = BlockArchiveReader.getInstance();
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = reader.fetchBlockAtHeight(testHeight);
|
||||
BlockData archivedBlockData = blockInfo.getA();
|
||||
ATStateData archivedAtStateData = blockInfo.getC().isEmpty() ? null : blockInfo.getC().get(0);
|
||||
List<TransactionData> archivedTransactions = blockInfo.getB();
|
||||
BlockTransformation blockInfo = reader.fetchBlockAtHeight(testHeight);
|
||||
BlockData archivedBlockData = blockInfo.getBlockData();
|
||||
ATStateData archivedAtStateData = blockInfo.getAtStates().isEmpty() ? null : blockInfo.getAtStates().get(0);
|
||||
List<TransactionData> archivedTransactions = blockInfo.getTransactions();
|
||||
|
||||
// Read the same block from the repository
|
||||
BlockData repositoryBlockData = repository.getBlockRepository().fromHeight(testHeight);
|
||||
@@ -255,7 +256,7 @@ public class BlockArchiveTests extends Common {
|
||||
|
||||
// Check block 10 (unarchived)
|
||||
BlockArchiveReader reader = BlockArchiveReader.getInstance();
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = reader.fetchBlockAtHeight(10);
|
||||
BlockTransformation blockInfo = reader.fetchBlockAtHeight(10);
|
||||
assertNull(blockInfo);
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
@@ -121,10 +122,10 @@ public class BlockTests extends Common {
|
||||
|
||||
assertEquals(BlockTransformer.getDataLength(block), bytes.length);
|
||||
|
||||
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = BlockTransformer.fromBytes(bytes);
|
||||
BlockTransformation blockInfo = BlockTransformer.fromBytes(bytes);
|
||||
|
||||
// Compare transactions
|
||||
List<TransactionData> deserializedTransactions = blockInfo.getB();
|
||||
List<TransactionData> deserializedTransactions = blockInfo.getTransactions();
|
||||
assertEquals("Transaction count differs", blockData.getTransactionCount(), deserializedTransactions.size());
|
||||
|
||||
for (int i = 0; i < blockData.getTransactionCount(); ++i) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.AES;
|
||||
import org.qortal.crypto.BouncyCastle25519;
|
||||
import org.qortal.crypto.Qortal25519Extras;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.utils.Base58;
|
||||
@@ -123,14 +123,14 @@ public class CryptoTests extends Common {
|
||||
random.nextBytes(ed25519PrivateKey);
|
||||
PrivateKeyAccount account = new PrivateKeyAccount(null, ed25519PrivateKey);
|
||||
|
||||
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(account.getPrivateKey());
|
||||
byte[] x25519PrivateKey = Qortal25519Extras.toX25519PrivateKey(account.getPrivateKey());
|
||||
X25519PrivateKeyParameters x25519PrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
|
||||
|
||||
// Derive X25519 public key from X25519 private key
|
||||
byte[] x25519PublicKeyFromPrivate = x25519PrivateKeyParams.generatePublicKey().getEncoded();
|
||||
|
||||
// Derive X25519 public key from Ed25519 public key
|
||||
byte[] x25519PublicKeyFromEd25519 = BouncyCastle25519.toX25519PublicKey(account.getPublicKey());
|
||||
byte[] x25519PublicKeyFromEd25519 = Qortal25519Extras.toX25519PublicKey(account.getPublicKey());
|
||||
|
||||
assertEquals(String.format("Public keys do not match, from private key %s", Base58.encode(ed25519PrivateKey)), Base58.encode(x25519PublicKeyFromPrivate), Base58.encode(x25519PublicKeyFromEd25519));
|
||||
}
|
||||
@@ -162,10 +162,10 @@ public class CryptoTests extends Common {
|
||||
}
|
||||
|
||||
private static byte[] calcBCSharedSecret(byte[] ed25519PrivateKey, byte[] ed25519PublicKey) {
|
||||
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(ed25519PrivateKey);
|
||||
byte[] x25519PrivateKey = Qortal25519Extras.toX25519PrivateKey(ed25519PrivateKey);
|
||||
X25519PrivateKeyParameters privateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
|
||||
|
||||
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(ed25519PublicKey);
|
||||
byte[] x25519PublicKey = Qortal25519Extras.toX25519PublicKey(ed25519PublicKey);
|
||||
X25519PublicKeyParameters publicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
|
||||
|
||||
byte[] sharedSecret = new byte[32];
|
||||
@@ -186,10 +186,10 @@ public class CryptoTests extends Common {
|
||||
final String expectedTheirX25519PublicKey = "ANjnZLRSzW9B1aVamiYGKP3XtBooU9tGGDjUiibUfzp2";
|
||||
final String expectedSharedSecret = "DTMZYG96x8XZuGzDvHFByVLsXedimqtjiXHhXPVe58Ap";
|
||||
|
||||
byte[] ourX25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(ourPrivateKey);
|
||||
byte[] ourX25519PrivateKey = Qortal25519Extras.toX25519PrivateKey(ourPrivateKey);
|
||||
assertEquals("X25519 private key incorrect", expectedOurX25519PrivateKey, Base58.encode(ourX25519PrivateKey));
|
||||
|
||||
byte[] theirX25519PublicKey = BouncyCastle25519.toX25519PublicKey(theirPublicKey);
|
||||
byte[] theirX25519PublicKey = Qortal25519Extras.toX25519PublicKey(theirPublicKey);
|
||||
assertEquals("X25519 public key incorrect", expectedTheirX25519PublicKey, Base58.encode(theirX25519PublicKey));
|
||||
|
||||
byte[] sharedSecret = calcBCSharedSecret(ourPrivateKey, theirPublicKey);
|
||||
|
||||
190
src/test/java/org/qortal/test/SchnorrTests.java
Normal file
190
src/test/java/org/qortal/test/SchnorrTests.java
Normal file
@@ -0,0 +1,190 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.Test;
|
||||
import org.qortal.crypto.Qortal25519Extras;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class SchnorrTests extends Qortal25519Extras {
|
||||
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
}
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
@Test
|
||||
public void testConversion() {
|
||||
// Scalar form
|
||||
byte[] scalarA = HashCode.fromString("0100000000000000000000000000000000000000000000000000000000000000".toLowerCase()).asBytes();
|
||||
System.out.printf("a: %s%n", HashCode.fromBytes(scalarA));
|
||||
|
||||
byte[] pointA = HashCode.fromString("5866666666666666666666666666666666666666666666666666666666666666".toLowerCase()).asBytes();
|
||||
|
||||
BigInteger expectedY = new BigInteger("46316835694926478169428394003475163141307993866256225615783033603165251855960");
|
||||
|
||||
PointAccum pointAccum = Qortal25519Extras.newPointAccum();
|
||||
scalarMultBase(scalarA, pointAccum);
|
||||
|
||||
byte[] encoded = new byte[POINT_BYTES];
|
||||
if (0 == encodePoint(pointAccum, encoded, 0))
|
||||
fail("Point encoding failed");
|
||||
|
||||
System.out.printf("aG: %s%n", HashCode.fromBytes(encoded));
|
||||
assertArrayEquals(pointA, encoded);
|
||||
|
||||
byte[] yBytes = new byte[POINT_BYTES];
|
||||
System.arraycopy(encoded,0, yBytes, 0, encoded.length);
|
||||
Bytes.reverse(yBytes);
|
||||
|
||||
System.out.printf("yBytes: %s%n", HashCode.fromBytes(yBytes));
|
||||
BigInteger yBI = new BigInteger(yBytes);
|
||||
|
||||
System.out.printf("aG y: %s%n", yBI);
|
||||
assertEquals(expectedY, yBI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddition() {
|
||||
/*
|
||||
* 1G: b'5866666666666666666666666666666666666666666666666666666666666666'
|
||||
* 2G: b'c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022'
|
||||
* 3G: b'd4b4f5784868c3020403246717ec169ff79e26608ea126a1ab69ee77d1b16712'
|
||||
*/
|
||||
|
||||
// Scalar form
|
||||
byte[] s1 = HashCode.fromString("0100000000000000000000000000000000000000000000000000000000000000".toLowerCase()).asBytes();
|
||||
byte[] s2 = HashCode.fromString("0200000000000000000000000000000000000000000000000000000000000000".toLowerCase()).asBytes();
|
||||
|
||||
// Point form
|
||||
byte[] g1 = HashCode.fromString("5866666666666666666666666666666666666666666666666666666666666666".toLowerCase()).asBytes();
|
||||
byte[] g2 = HashCode.fromString("c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022".toLowerCase()).asBytes();
|
||||
byte[] g3 = HashCode.fromString("d4b4f5784868c3020403246717ec169ff79e26608ea126a1ab69ee77d1b16712".toLowerCase()).asBytes();
|
||||
|
||||
PointAccum p1 = Qortal25519Extras.newPointAccum();
|
||||
scalarMultBase(s1, p1);
|
||||
|
||||
PointAccum p2 = Qortal25519Extras.newPointAccum();
|
||||
scalarMultBase(s2, p2);
|
||||
|
||||
pointAdd(pointCopy(p1), p2);
|
||||
|
||||
byte[] encoded = new byte[POINT_BYTES];
|
||||
if (0 == encodePoint(p2, encoded, 0))
|
||||
fail("Point encoding failed");
|
||||
|
||||
System.out.printf("sum: %s%n", HashCode.fromBytes(encoded));
|
||||
assertArrayEquals(g3, encoded);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleSign() {
|
||||
byte[] privateKey = HashCode.fromString("0100000000000000000000000000000000000000000000000000000000000000".toLowerCase()).asBytes();
|
||||
byte[] message = HashCode.fromString("01234567".toLowerCase()).asBytes();
|
||||
|
||||
byte[] signature = signForAggregation(privateKey, message);
|
||||
System.out.printf("signature: %s%n", HashCode.fromBytes(signature));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleVerify() {
|
||||
byte[] privateKey = HashCode.fromString("0100000000000000000000000000000000000000000000000000000000000000".toLowerCase()).asBytes();
|
||||
byte[] message = HashCode.fromString("01234567".toLowerCase()).asBytes();
|
||||
byte[] signature = HashCode.fromString("13e58e88f3df9e06637d2d5bbb814c028e3ba135494530b9d3b120bdb31168d62c70a37ae9cfba816fe6038ee1ce2fb521b95c4a91c7ff0bb1dd2e67733f2b0d".toLowerCase()).asBytes();
|
||||
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
Qortal25519Extras.generatePublicKey(privateKey, 0, publicKey, 0);
|
||||
|
||||
assertTrue(verifyAggregated(publicKey, signature, message));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleSignAndVerify() {
|
||||
byte[] privateKey = HashCode.fromString("0100000000000000000000000000000000000000000000000000000000000000".toLowerCase()).asBytes();
|
||||
byte[] message = HashCode.fromString("01234567".toLowerCase()).asBytes();
|
||||
|
||||
byte[] signature = signForAggregation(privateKey, message);
|
||||
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
Qortal25519Extras.generatePublicKey(privateKey, 0, publicKey, 0);
|
||||
|
||||
assertTrue(verifyAggregated(publicKey, signature, message));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleAggregate() {
|
||||
List<OnlineAccountData> onlineAccounts = generateOnlineAccounts(1);
|
||||
|
||||
byte[] aggregatePublicKey = aggregatePublicKeys(onlineAccounts.stream().map(OnlineAccountData::getPublicKey).collect(Collectors.toUnmodifiableList()));
|
||||
System.out.printf("Aggregate public key: %s%n", HashCode.fromBytes(aggregatePublicKey));
|
||||
|
||||
byte[] aggregateSignature = aggregateSignatures(onlineAccounts.stream().map(OnlineAccountData::getSignature).collect(Collectors.toUnmodifiableList()));
|
||||
System.out.printf("Aggregate signature: %s%n", HashCode.fromBytes(aggregateSignature));
|
||||
|
||||
OnlineAccountData onlineAccount = onlineAccounts.get(0);
|
||||
|
||||
assertArrayEquals(String.format("expected: %s, actual: %s", HashCode.fromBytes(onlineAccount.getPublicKey()), HashCode.fromBytes(aggregatePublicKey)), onlineAccount.getPublicKey(), aggregatePublicKey);
|
||||
assertArrayEquals(String.format("expected: %s, actual: %s", HashCode.fromBytes(onlineAccount.getSignature()), HashCode.fromBytes(aggregateSignature)), onlineAccount.getSignature(), aggregateSignature);
|
||||
|
||||
// This is the crucial test:
|
||||
long timestamp = onlineAccount.getTimestamp();
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
assertTrue(verifyAggregated(aggregatePublicKey, aggregateSignature, timestampBytes));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleAggregate() {
|
||||
List<OnlineAccountData> onlineAccounts = generateOnlineAccounts(5000);
|
||||
|
||||
byte[] aggregatePublicKey = aggregatePublicKeys(onlineAccounts.stream().map(OnlineAccountData::getPublicKey).collect(Collectors.toUnmodifiableList()));
|
||||
System.out.printf("Aggregate public key: %s%n", HashCode.fromBytes(aggregatePublicKey));
|
||||
|
||||
byte[] aggregateSignature = aggregateSignatures(onlineAccounts.stream().map(OnlineAccountData::getSignature).collect(Collectors.toUnmodifiableList()));
|
||||
System.out.printf("Aggregate signature: %s%n", HashCode.fromBytes(aggregateSignature));
|
||||
|
||||
OnlineAccountData onlineAccount = onlineAccounts.get(0);
|
||||
|
||||
// This is the crucial test:
|
||||
long timestamp = onlineAccount.getTimestamp();
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
assertTrue(verifyAggregated(aggregatePublicKey, aggregateSignature, timestampBytes));
|
||||
}
|
||||
|
||||
private List<OnlineAccountData> generateOnlineAccounts(int numAccounts) {
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
|
||||
|
||||
long timestamp = System.currentTimeMillis();
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
|
||||
for (int a = 0; a < numAccounts; ++a) {
|
||||
byte[] privateKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
SECURE_RANDOM.nextBytes(privateKey);
|
||||
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
Qortal25519Extras.generatePublicKey(privateKey, 0, publicKey, 0);
|
||||
|
||||
byte[] signature = signForAggregation(privateKey, timestampBytes);
|
||||
|
||||
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey));
|
||||
}
|
||||
|
||||
return onlineAccounts;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
public class RewardShareKeys {
|
||||
@@ -28,7 +29,7 @@ public class RewardShareKeys {
|
||||
PublicKeyAccount recipientAccount = new PublicKeyAccount(null, args.length > 1 ? Base58.decode(args[1]) : minterAccount.getPublicKey());
|
||||
|
||||
byte[] rewardSharePrivateKey = minterAccount.getRewardSharePrivateKey(recipientAccount.getPublicKey());
|
||||
byte[] rewardSharePublicKey = PrivateKeyAccount.toPublicKey(rewardSharePrivateKey);
|
||||
byte[] rewardSharePublicKey = Crypto.toPublicKey(rewardSharePrivateKey);
|
||||
|
||||
System.out.println(String.format("Minter account: %s", minterAccount.getAddress()));
|
||||
System.out.println(String.format("Minter's public key: %s", Base58.encode(minterAccount.getPublicKey())));
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||
@@ -40,12 +41,15 @@ public class AccountUtils {
|
||||
public static TransactionData createRewardShare(Repository repository, String minter, String recipient, int sharePercent) throws DataException {
|
||||
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, minter);
|
||||
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);
|
||||
return createRewardShare(repository, mintingAccount, recipientAccount, sharePercent);
|
||||
}
|
||||
|
||||
public static TransactionData createRewardShare(Repository repository, PrivateKeyAccount mintingAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException {
|
||||
byte[] reference = mintingAccount.getLastReference();
|
||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
||||
|
||||
byte[] rewardSharePrivateKey = mintingAccount.getRewardSharePrivateKey(recipientAccount.getPublicKey());
|
||||
byte[] rewardSharePublicKey = PrivateKeyAccount.toPublicKey(rewardSharePrivateKey);
|
||||
byte[] rewardSharePublicKey = Crypto.toPublicKey(rewardSharePrivateKey);
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, mintingAccount.getPublicKey(), fee, null);
|
||||
TransactionData transactionData = new RewardShareTransactionData(baseTransactionData, recipientAccount.getAddress(), rewardSharePublicKey, sharePercent);
|
||||
@@ -65,6 +69,15 @@ public class AccountUtils {
|
||||
return rewardSharePrivateKey;
|
||||
}
|
||||
|
||||
public static byte[] rewardShare(Repository repository, PrivateKeyAccount minterAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException {
|
||||
TransactionData transactionData = createRewardShare(repository, minterAccount, recipientAccount, sharePercent);
|
||||
|
||||
TransactionUtils.signAndMint(repository, transactionData, minterAccount);
|
||||
byte[] rewardSharePrivateKey = minterAccount.getRewardSharePrivateKey(recipientAccount.getPublicKey());
|
||||
|
||||
return rewardSharePrivateKey;
|
||||
}
|
||||
|
||||
public static Map<String, Map<Long, Long>> getBalances(Repository repository, long... assetIds) throws DataException {
|
||||
Map<String, Map<Long, Long>> balances = new HashMap<>();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -25,6 +26,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.asset.AssetData;
|
||||
@@ -61,6 +63,7 @@ public class Common {
|
||||
|
||||
|
||||
public static final String testSettingsFilename = "test-settings-v2.json";
|
||||
public static boolean shouldRetainRepositoryAfterTest = false;
|
||||
|
||||
static {
|
||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||
@@ -110,6 +113,12 @@ public class Common {
|
||||
return testAccountsByName.values().stream().map(account -> new TestAccount(repository, account)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static PrivateKeyAccount generateRandomSeedAccount(Repository repository) {
|
||||
byte[] seed = new byte[32];
|
||||
new SecureRandom().nextBytes(seed);
|
||||
return new PrivateKeyAccount(repository, seed);
|
||||
}
|
||||
|
||||
public static void useSettingsAndDb(String settingsFilename, boolean dbInMemory) throws DataException {
|
||||
closeRepository();
|
||||
|
||||
@@ -126,6 +135,7 @@ public class Common {
|
||||
|
||||
public static void useSettings(String settingsFilename) throws DataException {
|
||||
Common.useSettingsAndDb(settingsFilename, true);
|
||||
setShouldRetainRepositoryAfterTest(false);
|
||||
}
|
||||
|
||||
public static void useDefaultSettings() throws DataException {
|
||||
@@ -207,7 +217,16 @@ public class Common {
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
}
|
||||
|
||||
public static void setShouldRetainRepositoryAfterTest(boolean shouldRetain) {
|
||||
shouldRetainRepositoryAfterTest = shouldRetain;
|
||||
}
|
||||
|
||||
public static void deleteTestRepository() throws DataException {
|
||||
if (shouldRetainRepositoryAfterTest) {
|
||||
// Don't delete if we've requested to keep the db intact
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete repository directory if exists
|
||||
Path repositoryPath = Paths.get(Settings.getInstance().getRepositoryPath());
|
||||
try {
|
||||
|
||||
@@ -81,7 +81,7 @@ public class BitcoinTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() {
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
Long balance = bitcoin.getWalletBalance(xprv58);
|
||||
|
||||
@@ -81,7 +81,7 @@ public class DigibyteTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() {
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
|
||||
|
||||
Long balance = digibyte.getWalletBalance(xprv58);
|
||||
|
||||
@@ -81,7 +81,7 @@ public class DogecoinTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() {
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||
|
||||
Long balance = dogecoin.getWalletBalance(xprv58);
|
||||
|
||||
@@ -80,7 +80,7 @@ public class LitecoinTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() {
|
||||
public void testGetWalletBalance() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
|
||||
Long balance = litecoin.getWalletBalance(xprv58);
|
||||
|
||||
297
src/test/java/org/qortal/test/crosschain/PirateChainTests.java
Normal file
297
src/test/java/org/qortal/test/crosschain/PirateChainTests.java
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user