forked from Qortal/qortal
Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad0d8fac91 | ||
|
|
a8b58d2007 | ||
|
|
a099ecf55b | ||
|
|
6b91b0477d | ||
|
|
d7e7c1f48c | ||
|
|
7c5932a512 | ||
|
|
610a3fcf83 | ||
|
|
b329dc41bc | ||
|
|
ef249066cd | ||
|
|
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 | ||
|
|
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 | ||
|
|
987446cf7f | ||
|
|
d2fc705846 | ||
|
|
e393150e9c | ||
|
|
43bfd28bcd | ||
|
|
ca8f8a59f4 | ||
|
|
d086ade91f | ||
|
|
64d4c458ec | ||
|
|
2478450694 | ||
|
|
9f19a042e6 | ||
|
|
922ffcc0be | ||
|
|
f887fcafe3 | ||
|
|
48b562f71b | ||
|
|
33cffe45fd | ||
|
|
a0ce75a978 | ||
|
|
8d168f6ad4 | ||
|
|
0875c5bf3b | ||
|
|
8e71cbd822 | ||
|
|
9896ec2ba6 | ||
|
|
9f9a74809e | ||
|
|
acce81cdcd | ||
|
|
d72953ae78 | ||
|
|
551686c2de | ||
|
|
b73c041cc3 | ||
|
|
9e8d85285f | ||
|
|
f41fbb3b3d | ||
|
|
fa3a81575a | ||
|
|
6990766f75 | ||
|
|
b33afd99a5 | ||
|
|
3c2ba4a0ea | ||
|
|
ab0fc07ee9 | ||
|
|
001650d48e | ||
|
|
659431ebfd | ||
|
|
0a419cb105 | ||
|
|
a4d4d17b82 | ||
|
|
0829ff6908 | ||
|
|
2d1b0fd6d0 | ||
|
|
122539596d | ||
|
|
86015e59a1 | ||
|
|
1ea1e00344 | ||
|
|
adecb21ada | ||
|
|
dac484136f | ||
|
|
aaa0b25106 | ||
|
|
3409086978 | ||
|
|
6c201db3dd | ||
|
|
da47df0a25 | ||
|
|
eea215dacf | ||
|
|
0949271dda | ||
|
|
6bb9227159 | ||
|
|
a95a37277c | ||
|
|
48b9aa5c18 | ||
|
|
1d7203a6fb | ||
|
|
1030b00f0a | ||
|
|
0c16d1fc11 | ||
|
|
ed04375385 | ||
|
|
6e49d20383 | ||
|
|
dc34eed203 | ||
|
|
fbe4f3fad8 | ||
|
|
e7ee3a06c7 | ||
|
|
599877195b | ||
|
|
7f9d267992 | ||
|
|
52904db413 | ||
|
|
5e0bde226a | ||
|
|
0695039ee3 | ||
|
|
a4bcd4451c | ||
|
|
e5b4b61832 | ||
|
|
dd55dc277b | ||
|
|
81ef1ae964 | ||
|
|
46701e4de7 | ||
|
|
0f52ccb433 | ||
|
|
8aed84e6af | ||
|
|
568497e1c5 | ||
|
|
f9972f50e0 | ||
|
|
05d9a7e820 | ||
|
|
348f3c382e | ||
|
|
1da157d33f | ||
|
|
de4f004a08 | ||
|
|
522ef282c8 | ||
|
|
b5522ea260 | ||
|
|
b1f184c493 | ||
|
|
d66dd51bf6 | ||
|
|
0baed55a44 | ||
|
|
390b359761 | ||
|
|
70eaaa9e3b | ||
|
|
3e622f7185 | ||
|
|
3f12be50ac | ||
|
|
68412b49a1 | ||
|
|
c9b2620461 | ||
|
|
df3f16ccf1 | ||
|
|
22aa5c41b5 | ||
|
|
8e09567221 | ||
|
|
3505788d42 | ||
|
|
91e0c9b940 | ||
|
|
00996b047f | ||
|
|
44fc0f367d | ||
|
|
b0e6259073 | ||
|
|
6255b2a907 | ||
|
|
a5fb0be274 | ||
|
|
e835f6d998 | ||
|
|
54ff564bb1 | ||
|
|
f8a5ded0ba | ||
|
|
a1be66f02b | ||
|
|
0815ad2cf0 | ||
|
|
3484047ad4 | ||
|
|
a63fa1cce5 | ||
|
|
59119ebc3b | ||
|
|
276f1b7e68 | ||
|
|
c482e5b5ca | ||
|
|
8c3e0adf35 | ||
|
|
64ff3ac672 | ||
|
|
cfe92525ed | ||
|
|
0e3a9ee2b2 | ||
|
|
a921db2cc6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,6 +28,7 @@
|
||||
/WindowsInstaller/Install Files/qortal.jar
|
||||
/*.7z
|
||||
/tmp
|
||||
/wallets
|
||||
/data*
|
||||
/src/test/resources/arbitrary/*/.qortal/cache
|
||||
apikey.txt
|
||||
|
||||
@@ -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:{DEA09B3D-AFFA-409F-B208-E148E9A9005D} 1049:{79180B3D-8A6B-4DED-BD60-1A58F941E1DE} 2052:{90F65B96-22CD-41FA-82B0-E65183EA1EF9} 2057:{AB4872AC-E794-42BD-9305-8DFD06243A88} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{B786B6C1-86FA-4917-BAF9-7C9D10959D66} 1049:{60881A63-53FC-4DBE-AF3B-0568F55D2150} 2052:{108D1268-8111-49B9-B768-CC0A0A0CEDE1} 2057:{46DB692E-D942-40D5-B32E-FB94458478BF} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="3.2.3" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="3.4.0" 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="{63FD92A7-4AE2-46A0-9B83-EB27DA636C65}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{D57E945C-0FFB-447C-ADF7-2253CEBF4C0C}" 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"/>
|
||||
|
||||
6
pom.xml
6
pom.xml
@@ -3,11 +3,11 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<version>3.4.1</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
<altcoinj.version>bf9fb80</altcoinj.version>
|
||||
<altcoinj.version>6628cfd</altcoinj.version>
|
||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||
@@ -444,7 +444,7 @@
|
||||
</dependency>
|
||||
<!-- For Litecoin, etc. support, requires bitcoinj -->
|
||||
<dependency>
|
||||
<groupId>com.github.jjos2372</groupId>
|
||||
<groupId>com.github.qortal</groupId>
|
||||
<artifactId>altcoinj</artifactId>
|
||||
<version>${altcoinj.version}</version>
|
||||
</dependency>
|
||||
|
||||
@@ -37,7 +37,7 @@ public class ApplyUpdate {
|
||||
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
|
||||
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
|
||||
|
||||
private static final long CHECK_INTERVAL = 10 * 1000L; // ms
|
||||
private static final long CHECK_INTERVAL = 30 * 1000L; // ms
|
||||
private static final int MAX_ATTEMPTS = 12;
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -8,11 +8,13 @@ import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
|
||||
@@ -59,7 +61,17 @@ public class Account {
|
||||
// Balance manipulations - assetId is 0 for QORT
|
||||
|
||||
public long getConfirmedBalance(long assetId) throws DataException {
|
||||
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
|
||||
AccountBalanceData accountBalanceData;
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes request data from peers instead of the local db
|
||||
accountBalanceData = LiteNode.getInstance().fetchAccountBalance(this.address, assetId);
|
||||
}
|
||||
else {
|
||||
// All other node types fetch from the local db
|
||||
accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
|
||||
}
|
||||
|
||||
if (accountBalanceData == null)
|
||||
return 0;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public class NodeInfo {
|
||||
public long buildTimestamp;
|
||||
public String nodeId;
|
||||
public boolean isTestNet;
|
||||
public String type;
|
||||
|
||||
public NodeInfo() {
|
||||
}
|
||||
|
||||
@@ -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,29 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class DigibyteSendRequest {
|
||||
|
||||
@Schema(description = "Digibyte BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Recipient's Digibyte address ('legacy' P2PKH only)", example = "1DigByteEaterAddressDontSendf59kuE")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of DGB to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long digibyteAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 DGB (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
public DigibyteSendRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.qortal.api.model.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class RavencoinSendRequest {
|
||||
|
||||
@Schema(description = "Ravencoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||
public String xprv58;
|
||||
|
||||
@Schema(description = "Recipient's Ravencoin address ('legacy' P2PKH only)", example = "1RvnCoinEaterAddressDontSendf59kuE")
|
||||
public String receivingAddress;
|
||||
|
||||
@Schema(description = "Amount of RVN to send", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long ravencoinAmount;
|
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 RVN (100 sats) per byte", example = "0.00000100", type = "number")
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public Long feePerByte;
|
||||
|
||||
public RavencoinSendRequest() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import org.qortal.api.Security;
|
||||
import org.qortal.api.model.ApiOnlineAccount;
|
||||
import org.qortal.api.model.RewardShareKeyRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
@@ -109,18 +110,26 @@ public class AddressesResource {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
byte[] lastReference = null;
|
||||
AccountData accountData;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
lastReference = accountData.getReference();
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes request data from peers instead of the local db
|
||||
accountData = LiteNode.getInstance().fetchAccountData(address);
|
||||
}
|
||||
else {
|
||||
// All other node types request data from local db
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
accountData = repository.getAccountRepository().getAccount(address);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
byte[] lastReference = accountData.getReference();
|
||||
|
||||
if (lastReference == null || lastReference.length == 0)
|
||||
return "false";
|
||||
|
||||
@@ -119,10 +119,23 @@ public class AdminResource {
|
||||
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
||||
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
||||
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
|
||||
nodeInfo.type = getNodeType();
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
private String getNodeType() {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
return "lite";
|
||||
}
|
||||
else if (Settings.getInstance().isTopOnly()) {
|
||||
return "topOnly";
|
||||
}
|
||||
else {
|
||||
return "full";
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/status")
|
||||
@Operation(
|
||||
@@ -381,6 +394,10 @@ public class AdminResource {
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
) @QueryParam("offset") Integer offset, @Parameter(
|
||||
name = "tail",
|
||||
description = "Fetch most recent log lines",
|
||||
schema = @Schema(type = "boolean")
|
||||
) @QueryParam("tail") Boolean tail, @Parameter(
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
LoggerContext loggerContext = (LoggerContext) LogManager.getContext();
|
||||
@@ -396,6 +413,13 @@ public class AdminResource {
|
||||
if (reverse != null && reverse)
|
||||
logLines = Lists.reverse(logLines);
|
||||
|
||||
// Tail mode - return the last X lines (where X = limit)
|
||||
if (tail != null && tail) {
|
||||
if (limit != null && limit > 0) {
|
||||
offset = logLines.size() - limit;
|
||||
}
|
||||
}
|
||||
|
||||
// offset out of bounds?
|
||||
if (offset != null && (offset < 0 || offset >= logLines.size()))
|
||||
return "";
|
||||
@@ -416,7 +440,7 @@ public class AdminResource {
|
||||
|
||||
limit = Math.min(limit, logLines.size());
|
||||
|
||||
logLines.subList(limit - 1, logLines.size()).clear();
|
||||
logLines.subList(limit, logLines.size()).clear();
|
||||
|
||||
return String.join("\n", logLines);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -57,6 +57,7 @@ import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.ZipUtils;
|
||||
|
||||
@Path("/arbitrary")
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,177 @@
|
||||
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 java.util.List;
|
||||
|
||||
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 org.bitcoinj.core.Transaction;
|
||||
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.DigibyteSendRequest;
|
||||
import org.qortal.crosschain.Digibyte;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
|
||||
@Path("/crosschain/dgb")
|
||||
@Tag(name = "Cross-Chain (Digibyte)")
|
||||
public class CrossChainDigibyteResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@POST
|
||||
@Path("/walletbalance")
|
||||
@Operation(
|
||||
summary = "Returns DGB balance for hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "BIP32 'm' private/public key in base58",
|
||||
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(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 getDigibyteWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
if (!digibyte.isValidDeterministicKey(key58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
Long balance = digibyte.getWalletBalanceFromTransactions(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
return balance.toString();
|
||||
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/wallettransactions")
|
||||
@Operation(
|
||||
summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "BIP32 'm' private/public key in base58",
|
||||
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public List<SimpleTransaction> getDigibyteWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
if (!digibyte.isValidDeterministicKey(key58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
return digibyte.getWalletTransactions(key58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/send")
|
||||
@Operation(
|
||||
summary = "Sends DGB from hierarchical, deterministic BIP32 wallet to specific address",
|
||||
description = "Currently supports 'legacy' P2PKH Digibyte 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.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = DigibyteSendRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
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, DigibyteSendRequest digibyteSendRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (digibyteSendRequest.digibyteAmount <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
if (digibyteSendRequest.feePerByte != null && digibyteSendRequest.feePerByte <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
if (!digibyte.isValidAddress(digibyteSendRequest.receivingAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (!digibyte.isValidDeterministicKey(digibyteSendRequest.xprv58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
Transaction spendTransaction = digibyte.buildSpend(digibyteSendRequest.xprv58,
|
||||
digibyteSendRequest.receivingAddress,
|
||||
digibyteSendRequest.digibyteAmount,
|
||||
digibyteSendRequest.feePerByte);
|
||||
|
||||
if (spendTransaction == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
|
||||
|
||||
try {
|
||||
digibyte.broadcastTransaction(spendTransaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
|
||||
return spendTransaction.getTxId().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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 java.util.List;
|
||||
|
||||
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 org.bitcoinj.core.Transaction;
|
||||
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.RavencoinSendRequest;
|
||||
import org.qortal.crosschain.Ravencoin;
|
||||
import org.qortal.crosschain.ForeignBlockchainException;
|
||||
import org.qortal.crosschain.SimpleTransaction;
|
||||
|
||||
@Path("/crosschain/rvn")
|
||||
@Tag(name = "Cross-Chain (Ravencoin)")
|
||||
public class CrossChainRavencoinResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@POST
|
||||
@Path("/walletbalance")
|
||||
@Operation(
|
||||
summary = "Returns RVN balance for hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "BIP32 'm' private/public key in base58",
|
||||
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(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 getRavencoinWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
if (!ravencoin.isValidDeterministicKey(key58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
Long balance = ravencoin.getWalletBalanceFromTransactions(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
return balance.toString();
|
||||
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/wallettransactions")
|
||||
@Operation(
|
||||
summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
|
||||
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string",
|
||||
description = "BIP32 'm' private/public key in base58",
|
||||
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public List<SimpleTransaction> getRavencoinWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
if (!ravencoin.isValidDeterministicKey(key58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
try {
|
||||
return ravencoin.getWalletTransactions(key58);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/send")
|
||||
@Operation(
|
||||
summary = "Sends RVN from hierarchical, deterministic BIP32 wallet to specific address",
|
||||
description = "Currently only supports 'legacy' P2PKH Ravencoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = RavencoinSendRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
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, RavencoinSendRequest ravencoinSendRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (ravencoinSendRequest.ravencoinAmount <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
if (ravencoinSendRequest.feePerByte != null && ravencoinSendRequest.feePerByte <= 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
if (!ravencoin.isValidAddress(ravencoinSendRequest.receivingAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (!ravencoin.isValidDeterministicKey(ravencoinSendRequest.xprv58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
Transaction spendTransaction = ravencoin.buildSpend(ravencoinSendRequest.xprv58,
|
||||
ravencoinSendRequest.receivingAddress,
|
||||
ravencoinSendRequest.ravencoinAmount,
|
||||
ravencoinSendRequest.feePerByte);
|
||||
|
||||
if (spendTransaction == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
|
||||
|
||||
try {
|
||||
ravencoin.broadcastTransaction(spendTransaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
|
||||
return spendTransaction.getTxId().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()) {
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.NameSummary;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.BuyNameTransactionData;
|
||||
@@ -101,7 +102,14 @@ public class NamesResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<NameData> names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse);
|
||||
List<NameData> names;
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
names = LiteNode.getInstance().fetchAccountNames(address);
|
||||
}
|
||||
else {
|
||||
names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse);
|
||||
}
|
||||
|
||||
return names.stream().map(NameSummary::new).collect(Collectors.toList());
|
||||
} catch (DataException e) {
|
||||
@@ -126,10 +134,18 @@ public class NamesResource {
|
||||
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public NameData getName(@PathParam("name") String name) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
NameData nameData = repository.getNameRepository().fromName(name);
|
||||
NameData nameData;
|
||||
|
||||
if (nameData == null)
|
||||
if (Settings.getInstance().isLite()) {
|
||||
nameData = LiteNode.getInstance().fetchNameData(name);
|
||||
}
|
||||
else {
|
||||
nameData = repository.getNameRepository().fromName(name);
|
||||
}
|
||||
|
||||
if (nameData == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NAME_UNKNOWN);
|
||||
}
|
||||
|
||||
return nameData;
|
||||
} catch (ApiException e) {
|
||||
|
||||
@@ -20,6 +20,11 @@ import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.model.ConnectedPeer;
|
||||
import org.qortal.api.model.PeersSummary;
|
||||
@@ -127,9 +132,29 @@ public class PeersResource {
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ExecuteProduceConsume.StatsSnapshot getEngineStats(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
public ExecuteProduceConsume.StatsSnapshot getEngineStats(@HeaderParam(Security.API_KEY_HEADER) String apiKey, @QueryParam("newLoggingLevel") Level newLoggingLevel) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (newLoggingLevel != null) {
|
||||
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
final Configuration config = ctx.getConfiguration();
|
||||
|
||||
String epcClassName = "org.qortal.network.Network.NetworkProcessor";
|
||||
LoggerConfig loggerConfig = config.getLoggerConfig(epcClassName);
|
||||
LoggerConfig specificConfig = loggerConfig;
|
||||
|
||||
// We need a specific configuration for this logger,
|
||||
// otherwise we would change the level of all other loggers
|
||||
// having the original configuration as parent as well
|
||||
if (!loggerConfig.getName().equals(epcClassName)) {
|
||||
specificConfig = new LoggerConfig(epcClassName, newLoggingLevel, true);
|
||||
specificConfig.setParent(loggerConfig);
|
||||
config.addLogger(epcClassName, specificConfig);
|
||||
}
|
||||
specificConfig.setLevel(newLoggingLevel);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
return Network.getInstance().getStatsSnapshot();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -32,6 +33,8 @@ import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.model.SimpleTransactionSignRequest;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.LiteNode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.globalization.Translator;
|
||||
import org.qortal.repository.DataException;
|
||||
@@ -250,14 +253,29 @@ public class TransactionsResource {
|
||||
ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public List<TransactionData> getUnconfirmedTransactions(@Parameter(
|
||||
description = "A list of transaction types"
|
||||
) @QueryParam("txType") List<TransactionType> txTypes, @Parameter(
|
||||
description = "Transaction creator's base58 encoded public key"
|
||||
) @QueryParam("creator") String creatorPublicKey58, @Parameter(
|
||||
ref = "limit"
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
) @QueryParam("offset") Integer offset, @Parameter(
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
|
||||
// Decode public key if supplied
|
||||
byte[] creatorPublicKey = null;
|
||||
if (creatorPublicKey58 != null) {
|
||||
try {
|
||||
creatorPublicKey = Base58.decode(creatorPublicKey58);
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
|
||||
}
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getTransactionRepository().getUnconfirmedTransactions(limit, offset, reverse);
|
||||
return repository.getTransactionRepository().getUnconfirmedTransactions(txTypes, creatorPublicKey, limit, offset, reverse);
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
@@ -366,6 +384,73 @@ public class TransactionsResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/address/{address}")
|
||||
@Operation(
|
||||
summary = "Returns transactions for given address",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "transactions",
|
||||
content = @Content(
|
||||
array = @ArraySchema(
|
||||
schema = @Schema(
|
||||
implementation = TransactionData.class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public List<TransactionData> getAddressTransactions(@PathParam("address") String address,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
if (!Crypto.isValidAddress(address)) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
}
|
||||
|
||||
if (limit == null) {
|
||||
limit = 0;
|
||||
}
|
||||
if (offset == null) {
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
List<TransactionData> transactions;
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Fetch from network
|
||||
transactions = LiteNode.getInstance().fetchAccountTransactions(address, limit, offset);
|
||||
|
||||
// Sort the data, since we can't guarantee the order that a peer sent it in
|
||||
if (reverse) {
|
||||
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp).reversed());
|
||||
} else {
|
||||
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Fetch from local db
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null,
|
||||
null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, reverse);
|
||||
|
||||
// Expand signatures to transactions
|
||||
transactions = new ArrayList<>(signatures.size());
|
||||
for (byte[] signature : signatures) {
|
||||
transactions.add(repository.getTransactionRepository().fromSignature(signature));
|
||||
}
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
return transactions;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/unitfee")
|
||||
@Operation(
|
||||
@@ -638,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);
|
||||
|
||||
|
||||
@@ -94,14 +94,9 @@ public class ArbitraryDataFile {
|
||||
try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
|
||||
outputStream.write(fileContent);
|
||||
this.filePath = outputFilePath;
|
||||
// Verify hash
|
||||
if (!this.hash58.equals(this.digest58())) {
|
||||
LOGGER.error("Hash {} does not match file digest {} for signature: {}", this.hash58, this.digest58(), Base58.encode(signature));
|
||||
this.delete();
|
||||
throw new DataException("Data file digest validation failed");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DataException("Unable to write data to file");
|
||||
this.delete();
|
||||
throw new DataException(String.format("Unable to write data with hash %s: %s", this.hash58, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ public class ArbitraryDataRenderer {
|
||||
byte[] data = Files.readAllBytes(Paths.get(filePath)); // TODO: limit file size that can be read into memory
|
||||
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data);
|
||||
htmlParser.addAdditionalHeaderTags();
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' blob:");
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' blob:; img-src 'self' data: blob:;");
|
||||
response.setContentType(context.getMimeType(filename));
|
||||
response.setContentLength(htmlParser.getData().length);
|
||||
response.getOutputStream().write(htmlParser.getData());
|
||||
|
||||
@@ -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 */
|
||||
@@ -216,11 +223,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 +261,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 +287,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 +348,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 +373,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 +389,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 +1016,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;
|
||||
@@ -1194,7 +1241,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 +1255,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 +1439,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 +1654,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 {
|
||||
|
||||
@@ -68,8 +68,12 @@ public class BlockChain {
|
||||
atFindNextTransactionFix,
|
||||
newBlockSigHeight,
|
||||
shareBinFix,
|
||||
rewardShareLimitTimestamp,
|
||||
calcChainWeightTimestamp,
|
||||
transactionV5Timestamp;
|
||||
transactionV5Timestamp,
|
||||
transactionV6Timestamp,
|
||||
disableReferenceTimestamp,
|
||||
aggregateSignatureTimestamp;
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@@ -154,7 +158,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. */
|
||||
@@ -162,6 +166,13 @@ public class BlockChain {
|
||||
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
|
||||
private long onlineAccountSignaturesMaxLifetime;
|
||||
|
||||
/** 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. */
|
||||
@@ -363,8 +374,8 @@ public class BlockChain {
|
||||
return this.minAccountLevelToRewardShare;
|
||||
}
|
||||
|
||||
public int getMaxRewardSharesPerMintingAccount() {
|
||||
return this.maxRewardSharesPerMintingAccount;
|
||||
public int getMaxRewardSharesPerFounderMintingAccount() {
|
||||
return this.maxRewardSharesPerFounderMintingAccount;
|
||||
}
|
||||
|
||||
public int getFounderEffectiveMintingLevel() {
|
||||
@@ -397,6 +408,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();
|
||||
}
|
||||
@@ -405,6 +420,18 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.transactionV5Timestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getTransactionV6Timestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.transactionV6Timestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getDisableReferenceTimestamp() {
|
||||
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) {
|
||||
@@ -433,6 +460,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)
|
||||
|
||||
@@ -61,8 +61,12 @@ public class BlockMinter extends Thread {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("BlockMinter");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes do not mint
|
||||
return;
|
||||
}
|
||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Wipe existing unconfirmed transactions
|
||||
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||
|
||||
@@ -72,30 +76,31 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// Going to need this a lot...
|
||||
BlockRepository blockRepository = repository.getBlockRepository();
|
||||
BlockData previousBlockData = null;
|
||||
BlockData previousBlockData = null;
|
||||
|
||||
// Vars to keep track of blocks that were skipped due to chain weight
|
||||
byte[] parentSignatureForLastLowWeightBlock = null;
|
||||
Long timeOfLastLowWeightBlock = 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<>();
|
||||
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
|
||||
// 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) {
|
||||
if (isMintingPossible != wasMintingPossible)
|
||||
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
||||
|
||||
if (isMintingPossible != wasMintingPossible)
|
||||
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
||||
|
||||
wasMintingPossible = isMintingPossible;
|
||||
wasMintingPossible = isMintingPossible;
|
||||
|
||||
try {
|
||||
// Sleep for a while
|
||||
Thread.sleep(1000);
|
||||
|
||||
@@ -109,319 +114,338 @@ public class BlockMinter extends Thread {
|
||||
if (minLatestBlockTimestamp == null)
|
||||
continue;
|
||||
|
||||
// No online accounts? (e.g. during startup)
|
||||
if (OnlineAccountsManager.getInstance().getOnlineAccounts().isEmpty())
|
||||
// No online accounts for current timestamp? (e.g. during startup)
|
||||
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
|
||||
continue;
|
||||
|
||||
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
||||
// No minting accounts?
|
||||
if (mintingAccountsData.isEmpty())
|
||||
continue;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Going to need this a lot...
|
||||
BlockRepository blockRepository = repository.getBlockRepository();
|
||||
|
||||
// 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)
|
||||
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
||||
// No minting accounts?
|
||||
if (mintingAccountsData.isEmpty())
|
||||
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;
|
||||
// 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();
|
||||
|
||||
// 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
|
||||
final byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
|
||||
final 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.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"));
|
||||
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;
|
||||
}
|
||||
|
||||
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"));
|
||||
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
madi.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
newBlocks.add(newBlock);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No potential block candidates?
|
||||
if (newBlocks.isEmpty())
|
||||
continue;
|
||||
// Needs a mutable copy of the unmodifiableList
|
||||
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||
BlockData lastBlockData = blockRepository.getLastBlock();
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(Controller.hasMisbehaved);
|
||||
|
||||
boolean newBlockMinted = false;
|
||||
Block newBlock = null;
|
||||
// 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);
|
||||
|
||||
try {
|
||||
// Clear repository session state so we have latest view of data
|
||||
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()))
|
||||
// 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;
|
||||
|
||||
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;
|
||||
|
||||
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())));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
goodBlocks.add(testBlock);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// 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;
|
||||
|
||||
// Reattach newBlocks to new repository handle
|
||||
for (Block newBlock : newBlocks)
|
||||
newBlock.setRepository(repository);
|
||||
|
||||
// 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 (DataException e) {
|
||||
LOGGER.warn("Repository issue while running block minter", e);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,18 +576,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);
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.api.ApiService;
|
||||
import org.qortal.api.DomainMapService;
|
||||
import org.qortal.api.GatewayService;
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||
@@ -39,8 +40,11 @@ import org.qortal.controller.arbitrary.*;
|
||||
import org.qortal.controller.repository.PruneManager;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
@@ -58,6 +62,7 @@ import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.utils.*;
|
||||
|
||||
public class Controller extends Thread {
|
||||
@@ -108,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
|
||||
|
||||
@@ -178,6 +184,52 @@ public class Controller extends Thread {
|
||||
}
|
||||
public GetArbitraryMetadataMessageStats getArbitraryMetadataMessageStats = new GetArbitraryMetadataMessageStats();
|
||||
|
||||
public static class GetAccountMessageStats {
|
||||
public AtomicLong requests = new AtomicLong();
|
||||
public AtomicLong cacheHits = new AtomicLong();
|
||||
public AtomicLong unknownAccounts = new AtomicLong();
|
||||
|
||||
public GetAccountMessageStats() {
|
||||
}
|
||||
}
|
||||
public GetAccountMessageStats getAccountMessageStats = new GetAccountMessageStats();
|
||||
|
||||
public static class GetAccountBalanceMessageStats {
|
||||
public AtomicLong requests = new AtomicLong();
|
||||
public AtomicLong unknownAccounts = new AtomicLong();
|
||||
|
||||
public GetAccountBalanceMessageStats() {
|
||||
}
|
||||
}
|
||||
public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats();
|
||||
|
||||
public static class GetAccountTransactionsMessageStats {
|
||||
public AtomicLong requests = new AtomicLong();
|
||||
public AtomicLong unknownAccounts = new AtomicLong();
|
||||
|
||||
public GetAccountTransactionsMessageStats() {
|
||||
}
|
||||
}
|
||||
public GetAccountTransactionsMessageStats getAccountTransactionsMessageStats = new GetAccountTransactionsMessageStats();
|
||||
|
||||
public static class GetAccountNamesMessageStats {
|
||||
public AtomicLong requests = new AtomicLong();
|
||||
public AtomicLong unknownAccounts = new AtomicLong();
|
||||
|
||||
public GetAccountNamesMessageStats() {
|
||||
}
|
||||
}
|
||||
public GetAccountNamesMessageStats getAccountNamesMessageStats = new GetAccountNamesMessageStats();
|
||||
|
||||
public static class GetNameMessageStats {
|
||||
public AtomicLong requests = new AtomicLong();
|
||||
public AtomicLong unknownAccounts = new AtomicLong();
|
||||
|
||||
public GetNameMessageStats() {
|
||||
}
|
||||
}
|
||||
public GetNameMessageStats getNameMessageStats = new GetNameMessageStats();
|
||||
|
||||
public AtomicLong latestBlocksCacheRefills = new AtomicLong();
|
||||
|
||||
public StatsSnapshot() {
|
||||
@@ -362,23 +414,27 @@ public class Controller extends Thread {
|
||||
return; // Not System.exit() so that GUI can display error
|
||||
}
|
||||
|
||||
// Rebuild Names table and check database integrity (if enabled)
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildAllNames();
|
||||
if (Settings.getInstance().isNamesIntegrityCheckEnabled()) {
|
||||
namesDatabaseIntegrityCheck.runIntegrityCheck();
|
||||
}
|
||||
// If we have a non-lite node, we need to perform some startup actions
|
||||
if (!Settings.getInstance().isLite()) {
|
||||
|
||||
LOGGER.info("Validating blockchain");
|
||||
try {
|
||||
BlockChain.validate();
|
||||
// Rebuild Names table and check database integrity (if enabled)
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildAllNames();
|
||||
if (Settings.getInstance().isNamesIntegrityCheckEnabled()) {
|
||||
namesDatabaseIntegrityCheck.runIntegrityCheck();
|
||||
}
|
||||
|
||||
Controller.getInstance().refillLatestBlocksCache();
|
||||
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't validate blockchain", e);
|
||||
Gui.getInstance().fatalError("Blockchain validation issue", e);
|
||||
return; // Not System.exit() so that GUI can display error
|
||||
LOGGER.info("Validating blockchain");
|
||||
try {
|
||||
BlockChain.validate();
|
||||
|
||||
Controller.getInstance().refillLatestBlocksCache();
|
||||
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't validate blockchain", e);
|
||||
Gui.getInstance().fatalError("Blockchain validation issue", e);
|
||||
return; // Not System.exit() so that GUI can display error
|
||||
}
|
||||
}
|
||||
|
||||
// Import current trade bot states and minting accounts if they exist
|
||||
@@ -497,6 +553,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();
|
||||
@@ -594,10 +651,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
|
||||
@@ -660,29 +722,6 @@ public class Controller extends Thread {
|
||||
return lastMisbehaved != null && lastMisbehaved > NTP.getTime() - MISBEHAVIOUR_COOLOFF;
|
||||
};
|
||||
|
||||
/** True if peer has unknown height, lower height or same height and same block signature (unless we don't have their block signature). */
|
||||
public static Predicate<Peer> hasShorterBlockchain = peer -> {
|
||||
BlockData highestBlockData = getInstance().getChainTip();
|
||||
int ourHeight = highestBlockData.getHeight();
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
|
||||
// Ensure we have chain tip data for this peer
|
||||
if (peerChainTipData == null)
|
||||
return true;
|
||||
|
||||
// Remove if peer is at a lower height than us
|
||||
Integer peerHeight = peerChainTipData.getLastHeight();
|
||||
if (peerHeight == null || peerHeight < ourHeight)
|
||||
return true;
|
||||
|
||||
// Don't remove if peer is on a greater height chain than us, or if we don't have their block signature
|
||||
if (peerHeight > ourHeight || peerChainTipData.getLastBlockSignature() == null)
|
||||
return false;
|
||||
|
||||
// Remove if signatures match
|
||||
return Arrays.equals(peerChainTipData.getLastBlockSignature(), highestBlockData.getSignature());
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasNoRecentBlock = peer -> {
|
||||
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
@@ -755,19 +794,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 (this.isMintingPossible) {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||
SysTray.getInstance().setTrayIcon(2);
|
||||
if (Settings.getInstance().isLite()) {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "LITE_NODE");
|
||||
SysTray.getInstance().setTrayIcon(4);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -775,13 +819,21 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height) + "\n" + String.format("%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion);
|
||||
String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText);
|
||||
if (!Settings.getInstance().isLite()) {
|
||||
tooltip = tooltip.concat(String.format(" - %s %d", heightText, height));
|
||||
}
|
||||
tooltip = tooltip.concat(String.format("\n%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion));
|
||||
SysTray.getInstance().setToolTipText(tooltip);
|
||||
|
||||
this.callbackExecutor.execute(() -> {
|
||||
@@ -938,6 +990,11 @@ public class Controller extends Thread {
|
||||
// Callbacks for/from network
|
||||
|
||||
public void doNetworkBroadcast() {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes have nothing to broadcast
|
||||
return;
|
||||
}
|
||||
|
||||
Network network = Network.getInstance();
|
||||
|
||||
// Send (if outbound) / Request peer lists
|
||||
@@ -1184,6 +1241,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;
|
||||
@@ -1220,6 +1281,26 @@ public class Controller extends Thread {
|
||||
TradeBot.getInstance().onTradePresencesMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ACCOUNT:
|
||||
onNetworkGetAccountMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ACCOUNT_BALANCE:
|
||||
onNetworkGetAccountBalanceMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ACCOUNT_TRANSACTIONS:
|
||||
onNetworkGetAccountTransactionsMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ACCOUNT_NAMES:
|
||||
onNetworkGetAccountNamesMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_NAME:
|
||||
onNetworkGetNameMessage(peer, message);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
|
||||
break;
|
||||
@@ -1241,7 +1322,7 @@ public class Controller extends Thread {
|
||||
this.stats.getBlockMessageStats.cacheHits.incrementAndGet();
|
||||
|
||||
// We need to duplicate it to prevent multiple threads setting ID on the same message
|
||||
CachedBlockMessage clonedBlockMessage = cachedBlockMessage.cloneWithNewId(message.getId());
|
||||
CachedBlockMessage clonedBlockMessage = Message.cloneWithNewId(cachedBlockMessage, message.getId());
|
||||
|
||||
if (!peer.sendMessage(clonedBlockMessage))
|
||||
peer.disconnect("failed to send block");
|
||||
@@ -1297,10 +1378,21 @@ 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());
|
||||
|
||||
// This call also causes the other needed data to be pulled in from repository
|
||||
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
|
||||
@@ -1314,7 +1406,9 @@ public class Controller extends Thread {
|
||||
this.blockMessageCache.put(ByteArray.wrap(blockData.getSignature()), blockMessage);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while send block %s to peer %s", Base58.encode(signature), peer), e);
|
||||
LOGGER.error(String.format("Repository issue while sending block %s to peer %s", Base58.encode(signature), peer), e);
|
||||
} catch (TransformationException e) {
|
||||
LOGGER.error(String.format("Serialization issue while sending block %s to peer %s", Base58.encode(signature), peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1455,11 +1549,13 @@ public class Controller extends Thread {
|
||||
private void onNetworkHeightV2Message(Peer peer, Message message) {
|
||||
HeightV2Message heightV2Message = (HeightV2Message) message;
|
||||
|
||||
// If peer is inbound and we've not updated their height
|
||||
// then this is probably their initial HEIGHT_V2 message
|
||||
// so they need a corresponding HEIGHT_V2 message from us
|
||||
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
|
||||
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
|
||||
if (!Settings.getInstance().isLite()) {
|
||||
// If peer is inbound and we've not updated their height
|
||||
// then this is probably their initial HEIGHT_V2 message
|
||||
// so they need a corresponding HEIGHT_V2 message from us
|
||||
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
|
||||
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
|
||||
}
|
||||
|
||||
// Update peer chain tip data
|
||||
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
|
||||
@@ -1469,6 +1565,193 @@ public class Controller extends Thread {
|
||||
Synchronizer.getInstance().requestSync();
|
||||
}
|
||||
|
||||
private void onNetworkGetAccountMessage(Peer peer, Message message) {
|
||||
GetAccountMessage getAccountMessage = (GetAccountMessage) message;
|
||||
String address = getAccountMessage.getAddress();
|
||||
this.stats.getAccountMessageStats.requests.incrementAndGet();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
|
||||
if (accountData == null) {
|
||||
// We don't have this account
|
||||
this.stats.getAccountMessageStats.unknownAccounts.getAndIncrement();
|
||||
|
||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
||||
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s", peer, address));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
accountUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(accountUnknownMessage))
|
||||
peer.disconnect("failed to send account-unknown response");
|
||||
return;
|
||||
}
|
||||
|
||||
AccountMessage accountMessage = new AccountMessage(accountData);
|
||||
accountMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(accountMessage)) {
|
||||
peer.disconnect("failed to send account");
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while send account %s to peer %s", address, peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkGetAccountBalanceMessage(Peer peer, Message message) {
|
||||
GetAccountBalanceMessage getAccountBalanceMessage = (GetAccountBalanceMessage) message;
|
||||
String address = getAccountBalanceMessage.getAddress();
|
||||
long assetId = getAccountBalanceMessage.getAssetId();
|
||||
this.stats.getAccountBalanceMessageStats.requests.incrementAndGet();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(address, assetId);
|
||||
|
||||
if (accountBalanceData == null) {
|
||||
// We don't have this account
|
||||
this.stats.getAccountBalanceMessageStats.unknownAccounts.getAndIncrement();
|
||||
|
||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
||||
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_BALANCE request for unknown account %s and asset ID %d", peer, address, assetId));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
accountUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(accountUnknownMessage))
|
||||
peer.disconnect("failed to send account-unknown response");
|
||||
return;
|
||||
}
|
||||
|
||||
AccountBalanceMessage accountMessage = new AccountBalanceMessage(accountBalanceData);
|
||||
accountMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(accountMessage)) {
|
||||
peer.disconnect("failed to send account balance");
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while send balance for account %s and asset ID %d to peer %s", address, assetId, peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkGetAccountTransactionsMessage(Peer peer, Message message) {
|
||||
GetAccountTransactionsMessage getAccountTransactionsMessage = (GetAccountTransactionsMessage) message;
|
||||
String address = getAccountTransactionsMessage.getAddress();
|
||||
int limit = Math.min(getAccountTransactionsMessage.getLimit(), 100);
|
||||
int offset = getAccountTransactionsMessage.getOffset();
|
||||
this.stats.getAccountTransactionsMessageStats.requests.incrementAndGet();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null,
|
||||
null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, false);
|
||||
|
||||
// Expand signatures to transactions
|
||||
List<TransactionData> transactions = new ArrayList<>(signatures.size());
|
||||
for (byte[] signature : signatures) {
|
||||
transactions.add(repository.getTransactionRepository().fromSignature(signature));
|
||||
}
|
||||
|
||||
if (transactions == null) {
|
||||
// We don't have this account
|
||||
this.stats.getAccountTransactionsMessageStats.unknownAccounts.getAndIncrement();
|
||||
|
||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
||||
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_TRANSACTIONS request for unknown account %s", peer, address));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
accountUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(accountUnknownMessage))
|
||||
peer.disconnect("failed to send account-unknown response");
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionsMessage transactionsMessage = new TransactionsMessage(transactions);
|
||||
transactionsMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(transactionsMessage)) {
|
||||
peer.disconnect("failed to send account transactions");
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending transactions for account %s %d to peer %s", address, peer), e);
|
||||
} catch (MessageException e) {
|
||||
LOGGER.error(String.format("Message serialization issue while sending transactions for account %s %d to peer %s", address, peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkGetAccountNamesMessage(Peer peer, Message message) {
|
||||
GetAccountNamesMessage getAccountNamesMessage = (GetAccountNamesMessage) message;
|
||||
String address = getAccountNamesMessage.getAddress();
|
||||
this.stats.getAccountNamesMessageStats.requests.incrementAndGet();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<NameData> namesDataList = repository.getNameRepository().getNamesByOwner(address);
|
||||
|
||||
if (namesDataList == null) {
|
||||
// We don't have this account
|
||||
this.stats.getAccountNamesMessageStats.unknownAccounts.getAndIncrement();
|
||||
|
||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
||||
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_NAMES request for unknown account %s", peer, address));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
accountUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(accountUnknownMessage))
|
||||
peer.disconnect("failed to send account-unknown response");
|
||||
return;
|
||||
}
|
||||
|
||||
NamesMessage namesMessage = new NamesMessage(namesDataList);
|
||||
namesMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(namesMessage)) {
|
||||
peer.disconnect("failed to send account names");
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while send names for account %s to peer %s", address, peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onNetworkGetNameMessage(Peer peer, Message message) {
|
||||
GetNameMessage getNameMessage = (GetNameMessage) message;
|
||||
String name = getNameMessage.getName();
|
||||
this.stats.getNameMessageStats.requests.incrementAndGet();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
NameData nameData = repository.getNameRepository().fromName(name);
|
||||
|
||||
if (nameData == null) {
|
||||
// We don't have this account
|
||||
this.stats.getNameMessageStats.unknownAccounts.getAndIncrement();
|
||||
|
||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
||||
LOGGER.debug(() -> String.format("Sending 'name unknown' response to peer %s for GET_NAME request for unknown name %s", peer, name));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message nameUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
nameUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(nameUnknownMessage))
|
||||
peer.disconnect("failed to send name-unknown response");
|
||||
return;
|
||||
}
|
||||
|
||||
NamesMessage namesMessage = new NamesMessage(Arrays.asList(nameData));
|
||||
namesMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(namesMessage)) {
|
||||
peer.disconnect("failed to send name data");
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while send name %s to peer %s", name, peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Utilities
|
||||
|
||||
@@ -1520,6 +1803,11 @@ public class Controller extends Thread {
|
||||
* @return boolean - whether our node's blockchain is up to date or not
|
||||
*/
|
||||
public boolean isUpToDate(Long minLatestBlockTimestamp) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes are always "up to date"
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do we even have a vaguely recent block?
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return false;
|
||||
|
||||
189
src/main/java/org/qortal/controller/LiteNode.java
Normal file
189
src/main/java/org/qortal/controller/LiteNode.java
Normal file
@@ -0,0 +1,189 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.*;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
|
||||
import static org.qortal.network.message.MessageType.*;
|
||||
|
||||
public class LiteNode {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(LiteNode.class);
|
||||
|
||||
private static LiteNode instance;
|
||||
|
||||
|
||||
public Map<Integer, Long> pendingRequests = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
public int MAX_TRANSACTIONS_PER_MESSAGE = 100;
|
||||
|
||||
|
||||
public LiteNode() {
|
||||
|
||||
}
|
||||
|
||||
public static synchronized LiteNode getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new LiteNode();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch account data from peers for given QORT address
|
||||
* @param address - the QORT address to query
|
||||
* @return accountData - the account data for this address, or null if not retrieved
|
||||
*/
|
||||
public AccountData fetchAccountData(String address) {
|
||||
GetAccountMessage getAccountMessage = new GetAccountMessage(address);
|
||||
AccountMessage accountMessage = (AccountMessage) this.sendMessage(getAccountMessage, ACCOUNT);
|
||||
if (accountMessage == null) {
|
||||
return null;
|
||||
}
|
||||
return accountMessage.getAccountData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch account balance data from peers for given QORT address and asset ID
|
||||
* @param address - the QORT address to query
|
||||
* @return balance - the balance for this address and assetId, or null if not retrieved
|
||||
*/
|
||||
public AccountBalanceData fetchAccountBalance(String address, long assetId) {
|
||||
GetAccountBalanceMessage getAccountMessage = new GetAccountBalanceMessage(address, assetId);
|
||||
AccountBalanceMessage accountMessage = (AccountBalanceMessage) this.sendMessage(getAccountMessage, ACCOUNT_BALANCE);
|
||||
if (accountMessage == null) {
|
||||
return null;
|
||||
}
|
||||
return accountMessage.getAccountBalanceData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of transactions for given QORT address
|
||||
* @param address - the QORT address to query
|
||||
* @param limit - the maximum number of results to return
|
||||
* @param offset - the starting index
|
||||
* @return a list of TransactionData objects, or null if not retrieved
|
||||
*/
|
||||
public List<TransactionData> fetchAccountTransactions(String address, int limit, int offset) {
|
||||
List<TransactionData> allTransactions = new ArrayList<>();
|
||||
if (limit == 0) {
|
||||
limit = Integer.MAX_VALUE;
|
||||
}
|
||||
int batchSize = Math.min(limit, MAX_TRANSACTIONS_PER_MESSAGE);
|
||||
|
||||
while (allTransactions.size() < limit) {
|
||||
GetAccountTransactionsMessage getAccountTransactionsMessage = new GetAccountTransactionsMessage(address, batchSize, offset);
|
||||
TransactionsMessage transactionsMessage = (TransactionsMessage) this.sendMessage(getAccountTransactionsMessage, TRANSACTIONS);
|
||||
if (transactionsMessage == null) {
|
||||
// An error occurred, so give up instead of returning partial results
|
||||
return null;
|
||||
}
|
||||
allTransactions.addAll(transactionsMessage.getTransactions());
|
||||
if (transactionsMessage.getTransactions().size() < batchSize) {
|
||||
// No more transactions to fetch
|
||||
break;
|
||||
}
|
||||
offset += batchSize;
|
||||
}
|
||||
return allTransactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of names for given QORT address
|
||||
* @param address - the QORT address to query
|
||||
* @return a list of NameData objects, or null if not retrieved
|
||||
*/
|
||||
public List<NameData> fetchAccountNames(String address) {
|
||||
GetAccountNamesMessage getAccountNamesMessage = new GetAccountNamesMessage(address);
|
||||
NamesMessage namesMessage = (NamesMessage) this.sendMessage(getAccountNamesMessage, NAMES);
|
||||
if (namesMessage == null) {
|
||||
return null;
|
||||
}
|
||||
return namesMessage.getNameDataList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch info about a registered name
|
||||
* @param name - the name to query
|
||||
* @return a NameData object, or null if not retrieved
|
||||
*/
|
||||
public NameData fetchNameData(String name) {
|
||||
GetNameMessage getNameMessage = new GetNameMessage(name);
|
||||
NamesMessage namesMessage = (NamesMessage) this.sendMessage(getNameMessage, NAMES);
|
||||
if (namesMessage == null) {
|
||||
return null;
|
||||
}
|
||||
List<NameData> nameDataList = namesMessage.getNameDataList();
|
||||
if (nameDataList == null || nameDataList.size() != 1) {
|
||||
return null;
|
||||
}
|
||||
// We are only expecting a single item in the list
|
||||
return nameDataList.get(0);
|
||||
}
|
||||
|
||||
|
||||
private Message sendMessage(Message message, MessageType expectedResponseMessageType) {
|
||||
// This asks a random peer for the data
|
||||
// TODO: ask multiple peers, and disregard everything if there are any significant differences in the responses
|
||||
|
||||
// Needs a mutable copy of the unmodifiableList
|
||||
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||
|
||||
// Disregard peers that have "misbehaved" recently
|
||||
peers.removeIf(Controller.hasMisbehaved);
|
||||
|
||||
// Disregard peers that only have genesis block
|
||||
// TODO: peers.removeIf(Controller.hasOnlyGenesisBlock);
|
||||
|
||||
// Disregard peers that are on an old version
|
||||
peers.removeIf(Controller.hasOldVersion);
|
||||
|
||||
// Disregard peers that are on a known inferior chain tip
|
||||
// TODO: peers.removeIf(Controller.hasInferiorChainTip);
|
||||
|
||||
if (peers.isEmpty()) {
|
||||
LOGGER.info("No peers available to send {} message to", message.getType());
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pick random peer
|
||||
int index = new SecureRandom().nextInt(peers.size());
|
||||
Peer peer = peers.get(index);
|
||||
|
||||
LOGGER.info("Sending {} message to peer {}...", message.getType(), peer);
|
||||
|
||||
Message responseMessage;
|
||||
|
||||
try {
|
||||
responseMessage = peer.getResponse(message);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (responseMessage == null) {
|
||||
LOGGER.info("Peer {} didn't respond to {} message", peer, message.getType());
|
||||
return null;
|
||||
}
|
||||
else if (responseMessage.getType() != expectedResponseMessageType) {
|
||||
LOGGER.info("Peer responded with unexpected message type {} (should be {})", peer, responseMessage.getType(), expectedResponseMessageType);
|
||||
return null;
|
||||
}
|
||||
|
||||
LOGGER.info("Peer {} responded with {} message", peer, responseMessage.getType());
|
||||
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.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;
|
||||
|
||||
@@ -134,6 +127,11 @@ public class Synchronizer extends Thread {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Synchronizer");
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes don't need to sync
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (running && !Controller.isStopping()) {
|
||||
Thread.sleep(1000);
|
||||
@@ -173,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;
|
||||
}
|
||||
@@ -235,9 +233,6 @@ public class Synchronizer extends Thread {
|
||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||
peers.removeIf(Controller.hasInferiorChainTip);
|
||||
|
||||
// Remove peers with unknown height, lower height or same height and same block signature (unless we don't have their block signature)
|
||||
peers.removeIf(Controller.hasShorterBlockchain);
|
||||
|
||||
final int peersBeforeComparison = peers.size();
|
||||
|
||||
// Request recent block summaries from the remaining peers, and locate our common block with each
|
||||
@@ -622,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;
|
||||
}
|
||||
@@ -633,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())
|
||||
@@ -690,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
|
||||
@@ -845,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) {
|
||||
@@ -852,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) {
|
||||
@@ -872,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;
|
||||
}
|
||||
}
|
||||
@@ -895,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;
|
||||
}
|
||||
}
|
||||
@@ -1577,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 {
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.GetTransactionMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
@@ -11,13 +13,15 @@ import org.qortal.network.message.TransactionSignaturesMessage;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TransactionImporter extends Thread {
|
||||
|
||||
@@ -54,12 +58,16 @@ public class TransactionImporter extends Thread {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Transaction Importer");
|
||||
|
||||
try {
|
||||
while (!Controller.isStopping()) {
|
||||
Thread.sleep(1000L);
|
||||
Thread.sleep(500L);
|
||||
|
||||
// Process incoming transactions queue
|
||||
processIncomingTransactionsQueue();
|
||||
validateTransactionsInQueue();
|
||||
importTransactionsInQueue();
|
||||
|
||||
// Clean up invalid incoming transactions list
|
||||
cleanupInvalidTransactionsList(NTP.getTime());
|
||||
}
|
||||
@@ -86,7 +94,26 @@ public class TransactionImporter extends Thread {
|
||||
incomingTransactions.keySet().removeIf(t -> Arrays.equals(t.getSignature(), signature));
|
||||
}
|
||||
|
||||
private void processIncomingTransactionsQueue() {
|
||||
/**
|
||||
* Retrieve all pending unconfirmed transactions that have had their signatures validated.
|
||||
* @return a list of TransactionData objects, with valid signatures.
|
||||
*/
|
||||
private List<TransactionData> getCachedSigValidTransactions() {
|
||||
synchronized (this.incomingTransactions) {
|
||||
return this.incomingTransactions.entrySet().stream()
|
||||
.filter(t -> Boolean.TRUE.equals(t.getValue()))
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the signatures of any transactions pending import, then update their
|
||||
* entries in the queue to mark them as valid/invalid.
|
||||
*
|
||||
* No database lock is required.
|
||||
*/
|
||||
private void validateTransactionsInQueue() {
|
||||
if (this.incomingTransactions.isEmpty()) {
|
||||
// Nothing to do?
|
||||
return;
|
||||
@@ -103,8 +130,17 @@ public class TransactionImporter extends Thread {
|
||||
LOGGER.debug("Validating signatures in incoming transactions queue (size {})...", unvalidatedCount);
|
||||
}
|
||||
|
||||
// A list of all currently pending transactions that have valid signatures
|
||||
List<Transaction> sigValidTransactions = new ArrayList<>();
|
||||
|
||||
// A list of signatures that became valid in this round
|
||||
List<byte[]> newlyValidSignatures = new ArrayList<>();
|
||||
|
||||
boolean isLiteNode = Settings.getInstance().isLite();
|
||||
|
||||
// We need the latest block in order to check for expired transactions
|
||||
BlockData latestBlock = Controller.getInstance().getChainTip();
|
||||
|
||||
// Signature validation round - does not require blockchain lock
|
||||
for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) {
|
||||
// Quick exit?
|
||||
@@ -114,34 +150,59 @@ public class TransactionImporter extends Thread {
|
||||
|
||||
TransactionData transactionData = transactionEntry.getKey();
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
String signature58 = Base58.encode(transactionData.getSignature());
|
||||
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop expired transactions before they are considered "sig valid"
|
||||
if (latestBlock != null && transaction.getDeadline() <= latestBlock.getTimestamp()) {
|
||||
LOGGER.debug("Removing expired {} transaction {} from import queue", transactionData.getType().name(), signature58);
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
invalidUnconfirmedTransactions.put(signature58, (now + EXPIRED_TRANSACTION_RECHECK_INTERVAL));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only validate signature if we haven't already done so
|
||||
Boolean isSigValid = transactionEntry.getValue();
|
||||
if (!Boolean.TRUE.equals(isSigValid)) {
|
||||
if (!transaction.isSignatureValid()) {
|
||||
String signature58 = Base58.encode(transactionData.getSignature());
|
||||
if (isLiteNode) {
|
||||
// Lite nodes can't easily validate transactions, so for now we will have to assume that everything is valid
|
||||
sigValidTransactions.add(transaction);
|
||||
newlyValidSignatures.add(transactionData.getSignature());
|
||||
// Add mark signature as valid if transaction still exists in import queue
|
||||
incomingTransactions.computeIfPresent(transactionData, (k, v) -> Boolean.TRUE);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.trace("Ignoring {} transaction {} with invalid signature", transactionData.getType().name(), signature58);
|
||||
if (!transaction.isSignatureValid()) {
|
||||
LOGGER.debug("Ignoring {} transaction {} with invalid signature", transactionData.getType().name(), signature58);
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
|
||||
// Also add to invalidIncomingTransactions map
|
||||
Long now = NTP.getTime();
|
||||
now = NTP.getTime();
|
||||
if (now != null) {
|
||||
Long expiry = now + INVALID_TRANSACTION_RECHECK_INTERVAL;
|
||||
LOGGER.trace("Adding stale invalid transaction {} to invalidUnconfirmedTransactions...", signature58);
|
||||
LOGGER.trace("Adding invalid transaction {} to invalidUnconfirmedTransactions...", signature58);
|
||||
// Add to invalidUnconfirmedTransactions so that we don't keep requesting it
|
||||
invalidUnconfirmedTransactions.put(signature58, expiry);
|
||||
}
|
||||
|
||||
// We're done with this transaction
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
// Count the number that were validated in this round, for logging purposes
|
||||
validatedCount++;
|
||||
}
|
||||
|
||||
// Count the number that were validated in this round, for logging purposes
|
||||
validatedCount++;
|
||||
|
||||
// Add mark signature as valid if transaction still exists in import queue
|
||||
incomingTransactions.computeIfPresent(transactionData, (k, v) -> Boolean.TRUE);
|
||||
|
||||
// Signature validated in this round
|
||||
newlyValidSignatures.add(transactionData.getSignature());
|
||||
|
||||
} else {
|
||||
LOGGER.trace(() -> String.format("Transaction %s known to have valid signature", Base58.encode(transactionData.getSignature())));
|
||||
}
|
||||
@@ -154,30 +215,44 @@ public class TransactionImporter extends Thread {
|
||||
LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size());
|
||||
}
|
||||
|
||||
if (sigValidTransactions.isEmpty()) {
|
||||
// Don't bother locking if there are no new transactions to process
|
||||
return;
|
||||
if (!newlyValidSignatures.isEmpty()) {
|
||||
LOGGER.debug("Broadcasting {} newly valid signatures ahead of import", newlyValidSignatures.size());
|
||||
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyValidSignatures);
|
||||
Network.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
|
||||
}
|
||||
|
||||
if (Synchronizer.getInstance().isSyncRequested() || Synchronizer.getInstance().isSynchronizing()) {
|
||||
// Prioritize syncing, and don't attempt to lock
|
||||
// Signature validity is retained in the incomingTransactions map, to avoid the above work being wasted
|
||||
return;
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue while processing incoming transactions", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock(2, TimeUnit.SECONDS)) {
|
||||
// Signature validity is retained in the incomingTransactions map, to avoid the above work being wasted
|
||||
LOGGER.debug("Too busy to process incoming transactions queue");
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.debug("Interrupted when trying to acquire blockchain lock");
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Import any transactions in the queue that have valid signatures.
|
||||
*
|
||||
* A database lock is required.
|
||||
*/
|
||||
private void importTransactionsInQueue() {
|
||||
List<TransactionData> sigValidTransactions = this.getCachedSigValidTransactions();
|
||||
if (sigValidTransactions.isEmpty()) {
|
||||
// Don't bother locking if there are no new transactions to process
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Processing incoming transactions queue (size {})...", sigValidTransactions.size());
|
||||
if (Synchronizer.getInstance().isSyncRequested() || Synchronizer.getInstance().isSynchronizing()) {
|
||||
// Prioritize syncing, and don't attempt to lock
|
||||
return;
|
||||
}
|
||||
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock()) {
|
||||
LOGGER.debug("Too busy to import incoming transactions queue");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Importing incoming transactions queue (size {})...", sigValidTransactions.size());
|
||||
|
||||
int processedCount = 0;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Import transactions with valid signatures
|
||||
try {
|
||||
@@ -187,14 +262,15 @@ public class TransactionImporter extends Thread {
|
||||
}
|
||||
|
||||
if (Synchronizer.getInstance().isSyncRequestPending()) {
|
||||
LOGGER.debug("Breaking out of transaction processing with {} remaining, because a sync request is pending", sigValidTransactions.size() - i);
|
||||
LOGGER.debug("Breaking out of transaction importing with {} remaining, because a sync request is pending", sigValidTransactions.size() - i);
|
||||
return;
|
||||
}
|
||||
|
||||
Transaction transaction = sigValidTransactions.get(i);
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
TransactionData transactionData = sigValidTransactions.get(i);
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
Transaction.ValidationResult validationResult = transaction.importAsUnconfirmed();
|
||||
processedCount++;
|
||||
|
||||
switch (validationResult) {
|
||||
case TRANSACTION_ALREADY_EXISTS: {
|
||||
@@ -216,7 +292,7 @@ public class TransactionImporter extends Thread {
|
||||
// All other invalid cases:
|
||||
default: {
|
||||
final String signature58 = Base58.encode(transactionData.getSignature());
|
||||
LOGGER.trace(() -> String.format("Ignoring invalid (%s) %s transaction %s", validationResult.name(), transactionData.getType().name(), signature58));
|
||||
LOGGER.debug(() -> String.format("Ignoring invalid (%s) %s transaction %s", validationResult.name(), transactionData.getType().name(), signature58));
|
||||
|
||||
Long now = NTP.getTime();
|
||||
if (now != null && now - transactionData.getTimestamp() > INVALID_TRANSACTION_STALE_TIMEOUT) {
|
||||
@@ -239,12 +315,11 @@ public class TransactionImporter extends Thread {
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
}
|
||||
} finally {
|
||||
LOGGER.debug("Finished processing incoming transactions queue");
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s"));
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue while processing incoming transactions", e);
|
||||
LOGGER.error("Repository issue while importing incoming transactions", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,8 +352,18 @@ public class TransactionImporter extends Thread {
|
||||
byte[] signature = getTransactionMessage.getSignature();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
// Firstly check the sig-valid transactions that are currently queued for import
|
||||
TransactionData transactionData = this.getCachedSigValidTransactions().stream()
|
||||
.filter(t -> Arrays.equals(signature, t.getSignature()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (transactionData == null) {
|
||||
// Not found in import queue, so try the database
|
||||
transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
}
|
||||
|
||||
if (transactionData == null) {
|
||||
// Still not found - so we don't have this transaction
|
||||
LOGGER.debug(() -> String.format("Ignoring GET_TRANSACTION request from peer %s for unknown transaction %s", peer, Base58.encode(signature)));
|
||||
// Send no response at all???
|
||||
return;
|
||||
@@ -289,7 +374,9 @@ public class TransactionImporter extends Thread {
|
||||
if (!peer.sendMessage(transactionMessage))
|
||||
peer.disconnect("failed to send transaction");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while send transaction %s to peer %s", Base58.encode(signature), peer), e);
|
||||
LOGGER.error(String.format("Repository issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
|
||||
} catch (TransformationException e) {
|
||||
LOGGER.error(String.format("Serialization issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
@@ -283,8 +286,8 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
LOGGER.debug(String.format("Sending data file list request for signature %s with %d hashes to %d peers...", signature58, hashCount, handshakedPeers.size()));
|
||||
|
||||
// FUTURE: send our address as requestingPeer once enough peers have switched to the new protocol
|
||||
String requestingPeer = null; // Network.getInstance().getOurExternalIpAddressAndPort();
|
||||
// Send our address as requestingPeer, to allow for potential direct connections with seeds/peers
|
||||
String requestingPeer = Network.getInstance().getOurExternalIpAddressAndPort();
|
||||
|
||||
// Build request
|
||||
Message getArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, missingHashes, now, 0, requestingPeer);
|
||||
@@ -511,18 +514,24 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
// Bump requestHops if it exists
|
||||
if (requestHops != null) {
|
||||
arbitraryDataFileListMessage.setRequestHops(++requestHops);
|
||||
requestHops++;
|
||||
}
|
||||
|
||||
ArbitraryDataFileListMessage forwardArbitraryDataFileListMessage;
|
||||
|
||||
// Remove optional parameters if the requesting peer doesn't support it yet
|
||||
// A message with less statistical data is better than no message at all
|
||||
if (!requestingPeer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
|
||||
arbitraryDataFileListMessage.removeOptionalStats();
|
||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
} else {
|
||||
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);
|
||||
if (!requestingPeer.sendMessage(arbitraryDataFileListMessage)) {
|
||||
if (!requestingPeer.sendMessage(forwardArbitraryDataFileListMessage)) {
|
||||
requestingPeer.disconnect("failed to forward arbitrary data file list");
|
||||
}
|
||||
}
|
||||
@@ -631,6 +640,9 @@ public class ArbitraryDataFileListManager {
|
||||
// We should only respond if we have at least one hash
|
||||
if (hashes.size() > 0) {
|
||||
|
||||
// Firstly we should keep track of the requesting peer, to allow for potential direct connections later
|
||||
ArbitraryDataFileManager.getInstance().addRecentDataRequest(requestingPeer);
|
||||
|
||||
// We have all the chunks, so update requests map to reflect that we've sent it
|
||||
// There is no need to keep track of the request, as we can serve all the chunks
|
||||
if (allChunksExist) {
|
||||
@@ -639,16 +651,19 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
|
||||
String ourAddress = Network.getInstance().getOurExternalIpAddressAndPort();
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature,
|
||||
hashes, NTP.getTime(), 0, ourAddress, true);
|
||||
arbitraryDataFileListMessage.setId(message.getId());
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage;
|
||||
|
||||
// Remove optional parameters if the requesting peer doesn't support it yet
|
||||
// A message with less statistical data is better than no message at all
|
||||
if (!peer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
|
||||
arbitraryDataFileListMessage.removeOptionalStats();
|
||||
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
} else {
|
||||
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature,
|
||||
hashes, NTP.getTime(), 0, ourAddress, true);
|
||||
}
|
||||
|
||||
arbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(arbitraryDataFileListMessage)) {
|
||||
LOGGER.debug("Couldn't send list of hashes");
|
||||
peer.disconnect("failed to send list of hashes");
|
||||
@@ -670,8 +685,7 @@ public class ArbitraryDataFileListManager {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
|
||||
int requestHops = getArbitraryDataFileListMessage.getRequestHops();
|
||||
getArbitraryDataFileListMessage.setRequestHops(++requestHops);
|
||||
int requestHops = getArbitraryDataFileListMessage.getRequestHops() + 1;
|
||||
long totalRequestTime = now - requestTime;
|
||||
|
||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||
@@ -679,11 +693,15 @@ public class ArbitraryDataFileListManager {
|
||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||
// 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 : getArbitraryDataFileListMessage);
|
||||
broadcastPeer ->
|
||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
|
||||
);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import com.google.common.net.InetAddresses;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
@@ -7,7 +8,6 @@ import org.qortal.controller.Controller;
|
||||
import org.qortal.data.arbitrary.ArbitraryDirectConnectionInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryRelayInfo;
|
||||
import org.qortal.data.network.ArbitraryPeerData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.network.Network;
|
||||
@@ -55,6 +55,13 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
*/
|
||||
private List<ArbitraryDirectConnectionInfo> directConnectionInfo = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* Map to keep track of peers requesting QDN data that we hold.
|
||||
* Key = peer address string, value = time of last request.
|
||||
* This allows for additional "burst" connections beyond existing limits.
|
||||
*/
|
||||
private Map<String, Long> recentDataRequests = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
|
||||
public static int MAX_FILE_HASH_RESPONSES = 1000;
|
||||
|
||||
@@ -109,6 +116,9 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
final long directConnectionInfoMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT;
|
||||
directConnectionInfo.removeIf(entry -> entry.getTimestamp() < directConnectionInfoMinimumTimestamp);
|
||||
|
||||
final long recentDataRequestMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RECENT_DATA_REQUESTS_TIMEOUT;
|
||||
recentDataRequests.entrySet().removeIf(entry -> entry.getValue() < recentDataRequestMinimumTimestamp);
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +197,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
boolean fileAlreadyExists = existingFile.exists();
|
||||
String hash58 = Base58.encode(hash);
|
||||
Message message = null;
|
||||
ArbitraryDataFileMessage arbitraryDataFileMessage;
|
||||
|
||||
// Fetch the file if it doesn't exist locally
|
||||
if (!fileAlreadyExists) {
|
||||
@@ -195,10 +205,11 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
arbitraryDataFileRequests.put(hash58, NTP.getTime());
|
||||
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(signature, hash);
|
||||
|
||||
Message response = null;
|
||||
try {
|
||||
message = peer.getResponseWithTimeout(getArbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
|
||||
response = peer.getResponseWithTimeout(getArbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
|
||||
} catch (InterruptedException e) {
|
||||
// Will return below due to null message
|
||||
// Will return below due to null response
|
||||
}
|
||||
arbitraryDataFileRequests.remove(hash58);
|
||||
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
|
||||
@@ -206,22 +217,24 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
// We may need to remove the file list request, if we have all the files for this transaction
|
||||
this.handleFileListRequests(signature);
|
||||
|
||||
if (message == null) {
|
||||
LOGGER.debug("Received null message from peer {}", peer);
|
||||
if (response == null) {
|
||||
LOGGER.debug("Received null response from peer {}", peer);
|
||||
return null;
|
||||
}
|
||||
if (message.getType() != Message.MessageType.ARBITRARY_DATA_FILE) {
|
||||
LOGGER.debug("Received message with invalid type: {} from peer {}", message.getType(), peer);
|
||||
if (response.getType() != MessageType.ARBITRARY_DATA_FILE) {
|
||||
LOGGER.debug("Received response with invalid type: {} from peer {}", response.getType(), peer);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
ArbitraryDataFileMessage peersArbitraryDataFileMessage = (ArbitraryDataFileMessage) response;
|
||||
arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, peersArbitraryDataFileMessage.getArbitraryDataFile());
|
||||
} else {
|
||||
LOGGER.debug(String.format("File hash %s already exists, so skipping the request", hash58));
|
||||
arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, existingFile);
|
||||
}
|
||||
ArbitraryDataFileMessage arbitraryDataFileMessage = (ArbitraryDataFileMessage) message;
|
||||
|
||||
// We might want to forward the request to the peer that originally requested it
|
||||
this.handleArbitraryDataFileForwarding(requestingPeer, message, originalMessage);
|
||||
this.handleArbitraryDataFileForwarding(requestingPeer, arbitraryDataFileMessage, originalMessage);
|
||||
|
||||
boolean isRelayRequest = (requestingPeer != null);
|
||||
if (isRelayRequest) {
|
||||
@@ -488,6 +501,45 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
}
|
||||
|
||||
|
||||
// Peers requesting QDN data from us
|
||||
|
||||
/**
|
||||
* Add an address string of a peer that is trying to request data from us.
|
||||
* @param peerAddress
|
||||
*/
|
||||
public void addRecentDataRequest(String peerAddress) {
|
||||
if (peerAddress == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure to remove the port, since it isn't guaranteed to match next time
|
||||
String[] parts = peerAddress.split(":");
|
||||
if (parts.length == 0) {
|
||||
return;
|
||||
}
|
||||
String host = parts[0];
|
||||
if (!InetAddresses.isInetAddress(host)) {
|
||||
// Invalid host
|
||||
return;
|
||||
}
|
||||
|
||||
this.recentDataRequests.put(host, now);
|
||||
}
|
||||
|
||||
public boolean isPeerRequestingData(String peerAddressWithoutPort) {
|
||||
return this.recentDataRequests.containsKey(peerAddressWithoutPort);
|
||||
}
|
||||
|
||||
public boolean hasPendingDataRequest() {
|
||||
return !this.recentDataRequests.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
// Network handlers
|
||||
|
||||
public void onNetworkGetArbitraryDataFileMessage(Peer peer, Message message) {
|
||||
|
||||
@@ -47,6 +47,9 @@ public class ArbitraryDataManager extends Thread {
|
||||
/** Maximum time to hold direct peer connection information */
|
||||
public static final long ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT = 2 * 60 * 1000L; // ms
|
||||
|
||||
/** Maximum time to hold information about recent data requests that we can fulfil */
|
||||
public static final long ARBITRARY_RECENT_DATA_REQUESTS_TIMEOUT = 2 * 60 * 1000L; // ms
|
||||
|
||||
/** Maximum number of hops that an arbitrary signatures request is allowed to make */
|
||||
private static int ARBITRARY_SIGNATURES_REQUEST_MAX_HOPS = 3;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -338,9 +337,12 @@ public class ArbitraryMetadataManager {
|
||||
Peer requestingPeer = request.getB();
|
||||
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);
|
||||
if (!requestingPeer.sendMessage(arbitraryMetadataMessage)) {
|
||||
if (!requestingPeer.sendMessage(forwardArbitraryMetadataMessage)) {
|
||||
requestingPeer.disconnect("failed to forward arbitrary metadata");
|
||||
}
|
||||
}
|
||||
@@ -423,8 +425,7 @@ public class ArbitraryMetadataManager {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
long requestTime = getArbitraryMetadataMessage.getRequestTime();
|
||||
int requestHops = getArbitraryMetadataMessage.getRequestHops();
|
||||
getArbitraryMetadataMessage.setRequestHops(++requestHops);
|
||||
int requestHops = getArbitraryMetadataMessage.getRequestHops() + 1;
|
||||
long totalRequestTime = now - requestTime;
|
||||
|
||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||
@@ -432,11 +433,14 @@ public class ArbitraryMetadataManager {
|
||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||
// 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 : getArbitraryMetadataMessage);
|
||||
broadcastPeer ->
|
||||
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -19,6 +19,11 @@ public class AtStatesPruner implements Runnable {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("AT States pruner");
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Nothing to prune in lite mode
|
||||
return;
|
||||
}
|
||||
|
||||
boolean archiveMode = false;
|
||||
if (!Settings.getInstance().isTopOnly()) {
|
||||
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving
|
||||
|
||||
@@ -19,6 +19,11 @@ public class AtStatesTrimmer implements Runnable {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("AT States trimmer");
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Nothing to trim in lite mode
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public class BlockArchiver implements Runnable {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Block archiver");
|
||||
|
||||
if (!Settings.getInstance().isArchiveEnabled()) {
|
||||
if (!Settings.getInstance().isArchiveEnabled() || Settings.getInstance().isLite()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ public class BlockPruner implements Runnable {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Block pruner");
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Nothing to prune in lite mode
|
||||
return;
|
||||
}
|
||||
|
||||
boolean archiveMode = false;
|
||||
if (!Settings.getInstance().isTopOnly()) {
|
||||
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving
|
||||
|
||||
@@ -107,7 +107,7 @@ public class NamesDatabaseIntegrityCheck {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
|
||||
Name nameObj = new Name(repository, buyNameTransactionData.getName());
|
||||
if (nameObj != null && nameObj.getNameData() != null) {
|
||||
nameObj.buy(buyNameTransactionData);
|
||||
nameObj.buy(buyNameTransactionData, false);
|
||||
modificationCount++;
|
||||
LOGGER.trace("Processed BUY_NAME transaction for name {}", name);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Online Accounts trimmer");
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Nothing to trim in lite mode
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Don't even start trimming until initial rush has ended
|
||||
Thread.sleep(INITIAL_SLEEP_PERIOD);
|
||||
|
||||
@@ -0,0 +1,885 @@
|
||||
package org.qortal.controller.tradebot;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
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 BitcoinACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BitcoinACCTv3TradeBot.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 BitcoinACCTv3TradeBot 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 BitcoinACCTv3TradeBot() {
|
||||
}
|
||||
|
||||
public static synchronized BitcoinACCTv3TradeBot getInstance() {
|
||||
if (instance == null)
|
||||
instance = new BitcoinACCTv3TradeBot();
|
||||
|
||||
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 BTC.
|
||||
* <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 Bitcoin) 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'/Bitcoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||
* <li>QORT amount on offer by Bob</li>
|
||||
* <li>BTC 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);
|
||||
|
||||
// Convert Bitcoin receiving address into public key hash (we only support P2PKH at this time)
|
||||
Address bitcoinReceivingAddress;
|
||||
try {
|
||||
bitcoinReceivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
||||
} catch (AddressFormatException e) {
|
||||
throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
}
|
||||
if (bitcoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
|
||||
byte[] bitcoinReceivingAccountInfo = bitcoinReceivingAddress.getHash();
|
||||
|
||||
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/BTC ACCT";
|
||||
String description = "QORT/BTC cross-chain trade";
|
||||
String aTType = "ACCT";
|
||||
String tags = "ACCT QORT BTC";
|
||||
byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, 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, BitcoinACCTv3.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.BITCOIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
tradeBotCreateRequest.foreignAmount, null, null, null, bitcoinReceivingAccountInfo);
|
||||
|
||||
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 BTC to an existing offer.
|
||||
* <p>
|
||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||
* and access to a Bitcoin 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 Bitcoin 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 Bitcoin main-net)
|
||||
* or 'tprv' for (Bitcoin 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 Bitcoin amount expected by 'Bob'.
|
||||
* <p>
|
||||
* If the Bitcoin 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 xprv58 funded wallet xprv in base58
|
||||
* @return true if P2SH-A funding transaction successfully broadcast to Bitcoin network, false otherwise
|
||||
* @throws DataException
|
||||
*/
|
||||
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, 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
|
||||
|
||||
// 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, BitcoinACCTv3.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.BITCOIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
crossChainTradeData.expectedForeignAmount, xprv58, 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 = Bitcoin.getInstance().getP2shFee(now);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.debug("Couldn't estimate Bitcoin 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 = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||
String p2shAddress = Bitcoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||
|
||||
// Build transaction for funding P2SH-A
|
||||
Transaction p2shFundingTransaction = Bitcoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
||||
if (p2shFundingTransaction == null) {
|
||||
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
|
||||
return ResponseResult.BALANCE_ISSUE;
|
||||
}
|
||||
|
||||
try {
|
||||
Bitcoin.getInstance().broadcastTransaction(p2shFundingTransaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
// We couldn't fund P2SH-A at this time
|
||||
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = BitcoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), 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", p2shAddress));
|
||||
|
||||
return ResponseResult.OK;
|
||||
}
|
||||
|
||||
@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 = BitcoinACCTv3.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 Bitcoin 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;
|
||||
}
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.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 Bitcoin pubkeyhash and lockTime-A
|
||||
byte[] messageData = messageTransactionData.getData();
|
||||
BitcoinACCTv3.OfferMessageData offerMessageData = BitcoinACCTv3.extractOfferMessageData(messageData);
|
||||
if (offerMessageData == null)
|
||||
continue;
|
||||
|
||||
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
|
||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||
|
||||
// Determine P2SH-A address and confirm funded
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
|
||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, 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", p2shAddressA));
|
||||
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 = BitcoinACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, 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 BTC 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;
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
|
||||
// Refund P2SH-A if we've passed lockTime-A
|
||||
if (NTP.getTime() >= lockTimeA * 1000L) {
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, 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", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> atData.getIsFinished()
|
||||
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
|
||||
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
|
||||
|
||||
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 = BitcoinACCTv3.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 = BitcoinACCTv3.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 BTC 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 BTC funds from P2SH-A
|
||||
* to Bob's 'foreign'/Bitcoin trade legacy-format address, as derived from trade private key.
|
||||
* <p>
|
||||
* (This could potentially be 'improved' to send BTC 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 BTC
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] secretA = BitcoinACCTv3.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
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, 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: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
|
||||
bitcoin.broadcastTransaction(p2shRedeemTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String receivingAddress = bitcoin.pkhToAddress(receivingAccountInfo);
|
||||
|
||||
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;
|
||||
|
||||
Bitcoin bitcoin = Bitcoin.getInstance();
|
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = bitcoin.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTimeA)
|
||||
return;
|
||||
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, 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!", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
break;
|
||||
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
Address receiving = Address.fromString(bitcoin.getNetworkParameters(), receiveAddress);
|
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
||||
|
||||
bitcoin.broadcastTransaction(p2shRefundTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,885 @@
|
||||
package org.qortal.controller.tradebot;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
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 DigibyteACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv3TradeBot.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 DigibyteACCTv3TradeBot 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 DigibyteACCTv3TradeBot() {
|
||||
}
|
||||
|
||||
public static synchronized DigibyteACCTv3TradeBot getInstance() {
|
||||
if (instance == null)
|
||||
instance = new DigibyteACCTv3TradeBot();
|
||||
|
||||
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 DGB.
|
||||
* <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 Digibyte) 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'/Digibyte public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||
* <li>QORT amount on offer by Bob</li>
|
||||
* <li>DGB 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);
|
||||
|
||||
// Convert Digibyte receiving address into public key hash (we only support P2PKH at this time)
|
||||
Address digibyteReceivingAddress;
|
||||
try {
|
||||
digibyteReceivingAddress = Address.fromString(Digibyte.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
||||
} catch (AddressFormatException e) {
|
||||
throw new DataException("Unsupported Digibyte receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
}
|
||||
if (digibyteReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw new DataException("Unsupported Digibyte receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
|
||||
byte[] digibyteReceivingAccountInfo = digibyteReceivingAddress.getHash();
|
||||
|
||||
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/DGB ACCT";
|
||||
String description = "QORT/DGB cross-chain trade";
|
||||
String aTType = "ACCT";
|
||||
String tags = "ACCT QORT DGB";
|
||||
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, 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, DigibyteACCTv3.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.DIGIBYTE.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
tradeBotCreateRequest.foreignAmount, null, null, null, digibyteReceivingAccountInfo);
|
||||
|
||||
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 DGB to an existing offer.
|
||||
* <p>
|
||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||
* and access to a Digibyte 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 Digibyte 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 Digibyte main-net)
|
||||
* or 'tprv' for (Digibyte 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 Digibyte amount expected by 'Bob'.
|
||||
* <p>
|
||||
* If the Digibyte 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 xprv58 funded wallet xprv in base58
|
||||
* @return true if P2SH-A funding transaction successfully broadcast to Digibyte network, false otherwise
|
||||
* @throws DataException
|
||||
*/
|
||||
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, 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
|
||||
|
||||
// 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, DigibyteACCTv3.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.DIGIBYTE.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
crossChainTradeData.expectedForeignAmount, xprv58, 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 = Digibyte.getInstance().getP2shFee(now);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.debug("Couldn't estimate Digibyte 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 = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||
String p2shAddress = Digibyte.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||
|
||||
// Build transaction for funding P2SH-A
|
||||
Transaction p2shFundingTransaction = Digibyte.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
||||
if (p2shFundingTransaction == null) {
|
||||
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
|
||||
return ResponseResult.BALANCE_ISSUE;
|
||||
}
|
||||
|
||||
try {
|
||||
Digibyte.getInstance().broadcastTransaction(p2shFundingTransaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
// We couldn't fund P2SH-A at this time
|
||||
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = DigibyteACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), 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", p2shAddress));
|
||||
|
||||
return ResponseResult.OK;
|
||||
}
|
||||
|
||||
@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 = DigibyteACCTv3.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 Digibyte 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;
|
||||
}
|
||||
|
||||
Digibyte digibyte = Digibyte.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 Digibyte pubkeyhash and lockTime-A
|
||||
byte[] messageData = messageTransactionData.getData();
|
||||
DigibyteACCTv3.OfferMessageData offerMessageData = DigibyteACCTv3.extractOfferMessageData(messageData);
|
||||
if (offerMessageData == null)
|
||||
continue;
|
||||
|
||||
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDigibytePKH;
|
||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||
|
||||
// Determine P2SH-A address and confirm funded
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||
String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
|
||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, 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", p2shAddressA));
|
||||
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 = DigibyteACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, 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 DGB 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;
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
|
||||
// Refund P2SH-A if we've passed lockTime-A
|
||||
if (NTP.getTime() >= lockTimeA * 1000L) {
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, 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", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> atData.getIsFinished()
|
||||
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
|
||||
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
|
||||
|
||||
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 = DigibyteACCTv3.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 = DigibyteACCTv3.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 DGB 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 DGB funds from P2SH-A
|
||||
* to Bob's 'foreign'/Digibyte trade legacy-format address, as derived from trade private key.
|
||||
* <p>
|
||||
* (This could potentially be 'improved' to send DGB 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 DGB
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] secretA = DigibyteACCTv3.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
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||
String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, 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: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(digibyte.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
|
||||
digibyte.broadcastTransaction(p2shRedeemTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String receivingAddress = digibyte.pkhToAddress(receivingAccountInfo);
|
||||
|
||||
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;
|
||||
|
||||
Digibyte digibyte = Digibyte.getInstance();
|
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = digibyte.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTimeA)
|
||||
return;
|
||||
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, 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!", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
break;
|
||||
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = digibyte.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
Address receiving = Address.fromString(digibyte.getNetworkParameters(), receiveAddress);
|
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(digibyte.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
||||
|
||||
digibyte.broadcastTransaction(p2shRefundTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,885 @@
|
||||
package org.qortal.controller.tradebot;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
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 RavencoinACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(RavencoinACCTv3TradeBot.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 RavencoinACCTv3TradeBot 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 RavencoinACCTv3TradeBot() {
|
||||
}
|
||||
|
||||
public static synchronized RavencoinACCTv3TradeBot getInstance() {
|
||||
if (instance == null)
|
||||
instance = new RavencoinACCTv3TradeBot();
|
||||
|
||||
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 RVN.
|
||||
* <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 Ravencoin) 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'/Ravencoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||
* <li>QORT amount on offer by Bob</li>
|
||||
* <li>RVN 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);
|
||||
|
||||
// Convert Ravencoin receiving address into public key hash (we only support P2PKH at this time)
|
||||
Address ravencoinReceivingAddress;
|
||||
try {
|
||||
ravencoinReceivingAddress = Address.fromString(Ravencoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
||||
} catch (AddressFormatException e) {
|
||||
throw new DataException("Unsupported Ravencoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
}
|
||||
if (ravencoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw new DataException("Unsupported Ravencoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
|
||||
byte[] ravencoinReceivingAccountInfo = ravencoinReceivingAddress.getHash();
|
||||
|
||||
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/RVN ACCT";
|
||||
String description = "QORT/RVN cross-chain trade";
|
||||
String aTType = "ACCT";
|
||||
String tags = "ACCT QORT RVN";
|
||||
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, 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, RavencoinACCTv3.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.RAVENCOIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
tradeBotCreateRequest.foreignAmount, null, null, null, ravencoinReceivingAccountInfo);
|
||||
|
||||
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 RVN to an existing offer.
|
||||
* <p>
|
||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||
* and access to a Ravencoin 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 Ravencoin 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 Ravencoin main-net)
|
||||
* or 'tprv' for (Ravencoin 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 Ravencoin amount expected by 'Bob'.
|
||||
* <p>
|
||||
* If the Ravencoin 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 xprv58 funded wallet xprv in base58
|
||||
* @return true if P2SH-A funding transaction successfully broadcast to Ravencoin network, false otherwise
|
||||
* @throws DataException
|
||||
*/
|
||||
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, 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
|
||||
|
||||
// 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, RavencoinACCTv3.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.RAVENCOIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
crossChainTradeData.expectedForeignAmount, xprv58, 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 = Ravencoin.getInstance().getP2shFee(now);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.debug("Couldn't estimate Ravencoin 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 = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||
String p2shAddress = Ravencoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||
|
||||
// Build transaction for funding P2SH-A
|
||||
Transaction p2shFundingTransaction = Ravencoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
||||
if (p2shFundingTransaction == null) {
|
||||
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
|
||||
return ResponseResult.BALANCE_ISSUE;
|
||||
}
|
||||
|
||||
try {
|
||||
Ravencoin.getInstance().broadcastTransaction(p2shFundingTransaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
// We couldn't fund P2SH-A at this time
|
||||
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = RavencoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), 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", p2shAddress));
|
||||
|
||||
return ResponseResult.OK;
|
||||
}
|
||||
|
||||
@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 = RavencoinACCTv3.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 Ravencoin 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;
|
||||
}
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.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 Ravencoin pubkeyhash and lockTime-A
|
||||
byte[] messageData = messageTransactionData.getData();
|
||||
RavencoinACCTv3.OfferMessageData offerMessageData = RavencoinACCTv3.extractOfferMessageData(messageData);
|
||||
if (offerMessageData == null)
|
||||
continue;
|
||||
|
||||
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerRavencoinPKH;
|
||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||
|
||||
// Determine P2SH-A address and confirm funded
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||
String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
|
||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, 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", p2shAddressA));
|
||||
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 = RavencoinACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, 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 RVN 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;
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
|
||||
// Refund P2SH-A if we've passed lockTime-A
|
||||
if (NTP.getTime() >= lockTimeA * 1000L) {
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, 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", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> atData.getIsFinished()
|
||||
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
|
||||
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
|
||||
|
||||
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 = RavencoinACCTv3.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 = RavencoinACCTv3.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 RVN 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 RVN funds from P2SH-A
|
||||
* to Bob's 'foreign'/Ravencoin trade legacy-format address, as derived from trade private key.
|
||||
* <p>
|
||||
* (This could potentially be 'improved' to send RVN 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 RVN
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] secretA = RavencoinACCTv3.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
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||
String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, 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: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(ravencoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
|
||||
ravencoin.broadcastTransaction(p2shRedeemTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String receivingAddress = ravencoin.pkhToAddress(receivingAccountInfo);
|
||||
|
||||
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;
|
||||
|
||||
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = ravencoin.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTimeA)
|
||||
return;
|
||||
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, 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!", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
break;
|
||||
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = ravencoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
Address receiving = Address.fromString(ravencoin.getNetworkParameters(), receiveAddress);
|
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(ravencoin.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
||||
|
||||
ravencoin.broadcastTransaction(p2shRefundTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,12 +94,15 @@ public class TradeBot implements Listener {
|
||||
private static final Map<Class<? extends ACCT>, Supplier<AcctTradeBot>> acctTradeBotSuppliers = new HashMap<>();
|
||||
static {
|
||||
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(BitcoinACCTv3.class, BitcoinACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(LitecoinACCTv2.class, LitecoinACCTv2TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
|
||||
}
|
||||
|
||||
private static TradeBot instance;
|
||||
@@ -239,8 +242,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;
|
||||
|
||||
@@ -289,7 +292,7 @@ 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) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.Map;
|
||||
|
||||
import org.bitcoinj.core.Context;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
@@ -18,10 +19,12 @@ public class Bitcoin extends Bitcoiny {
|
||||
|
||||
public static final String CURRENCY_CODE = "BTC";
|
||||
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
|
||||
private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
|
||||
private static final long NEW_FEE_AMOUNT = 10_000L;
|
||||
private static final long NEW_FEE_AMOUNT = 6_000L;
|
||||
|
||||
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||
|
||||
@@ -182,6 +185,11 @@ public class Bitcoin extends Bitcoiny {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
}
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
/**
|
||||
@@ -195,4 +203,17 @@ public class Bitcoin extends Bitcoiny {
|
||||
return this.bitcoinNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt> using 20 sat/byte fee.
|
||||
*
|
||||
* @param xprv58 BIP32 private key
|
||||
* @param recipient P2PKH address
|
||||
* @param amount unscaled amount
|
||||
* @return transaction, or null if insufficient funds
|
||||
*/
|
||||
@Override
|
||||
public Transaction buildSpend(String xprv58, String recipient, long amount) {
|
||||
return buildSpend(xprv58, recipient, amount, 20L);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
858
src/main/java/org/qortal/crosschain/BitcoinACCTv3.java
Normal file
858
src/main/java/org/qortal/crosschain/BitcoinACCTv3.java
Normal file
@@ -0,0 +1,858 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
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 Bitcoin & 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 Bitcoin & Qortal 'trade' keys</li>
|
||||
* <li>Alice funds Bitcoin 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' Bitcoin PKH</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 Bitcoin PKH</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 Bitcoin trade key and secret-A</li>
|
||||
* <li>P2SH-A BTC funds end up at Bitcoin address determined by redeem transaction output(s)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public class BitcoinACCTv3 implements ACCT {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BitcoinACCTv3.class);
|
||||
|
||||
public static final String NAME = BitcoinACCTv3.class.getSimpleName();
|
||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("676fb9350708dafa054eb0262d655039e393c1eb4918ec582f8d45524c9b4860").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 = 61;
|
||||
/** <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[] partnerBitcoinPKH;
|
||||
public byte[] hashOfSecretA;
|
||||
public long lockTimeA;
|
||||
}
|
||||
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerBitcoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
|
||||
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
|
||||
+ 24 /*partner's Bitcoin PKH (padded from 20 to 24)*/
|
||||
+ 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 BitcoinACCTv3 instance;
|
||||
|
||||
private BitcoinACCTv3() {
|
||||
}
|
||||
|
||||
public static synchronized BitcoinACCTv3 getInstance() {
|
||||
if (instance == null)
|
||||
instance = new BitcoinACCTv3();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCodeBytesHash() {
|
||||
return CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModeByteOffset() {
|
||||
return MODE_BYTE_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignBlockchain getBlockchain() {
|
||||
return Bitcoin.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 bitcoinPublicKeyHash 20-byte HASH160 of creator's trade Bitcoin public key
|
||||
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
||||
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
|
||||
* @param tradeTimeout suggested timeout for entire trade
|
||||
*/
|
||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, long qortAmount, long bitcoinAmount, int tradeTimeout) {
|
||||
if (bitcoinPublicKeyHash.length != 20)
|
||||
throw new IllegalArgumentException("Bitcoin public key hash should be 20 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 addrBitcoinPublicKeyHash = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrQortAmount = addrCounter++;
|
||||
final int addrBitcoinAmount = 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 addrTradeMessagePartnerBitcoinPKHOffset = addrCounter++;
|
||||
final int addrPartnerBitcoinPKHPointer = addrCounter++;
|
||||
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 addrPartnerBitcoinPKH = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
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));
|
||||
|
||||
// Bitcoin public key hash
|
||||
assert dataByteBuffer.position() == addrBitcoinPublicKeyHash * MachineState.VALUE_SIZE : "addrBitcoinPublicKeyHash incorrect";
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(bitcoinPublicKeyHash, 32, 0));
|
||||
|
||||
// Redeem Qort amount
|
||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||
dataByteBuffer.putLong(qortAmount);
|
||||
|
||||
// Expected Bitcoin amount
|
||||
assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect";
|
||||
dataByteBuffer.putLong(bitcoinAmount);
|
||||
|
||||
// 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 partner's Bitcoin PKH
|
||||
assert dataByteBuffer.position() == addrTradeMessagePartnerBitcoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerBitcoinPKHOffset incorrect";
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
// Index into data segment of partner's Bitcoin PKH, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerBitcoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerBitcoinPKHPointer incorrect";
|
||||
dataByteBuffer.putLong(addrPartnerBitcoinPKH);
|
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
||||
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
|
||||
dataByteBuffer.putLong(64L);
|
||||
|
||||
// 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 trade partner's Bitcoin public key hash (PKH) from message into B
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerBitcoinPKHOffset));
|
||||
// Store partner's Bitcoin PKH (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerBitcoinPKHPointer));
|
||||
// Extract AT trade timeout (minutes) (from B4)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, 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();
|
||||
|
||||
/* NOP - to ensure BITCOIN ACCT is unique */
|
||||
codeByteBuffer.put(OpCode.NOP.compile());
|
||||
|
||||
// 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 BTC-QORT ACCT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
assert Arrays.equals(Crypto.digest(codeBytes), BitcoinACCTv3.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.BITCOIN.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 Bitcoin/foreign public key hash
|
||||
tradeData.creatorForeignPKH = new byte[20];
|
||||
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||
|
||||
// We don't use secret-B
|
||||
tradeData.hashOfSecretB = null;
|
||||
|
||||
// Redeem payout
|
||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Expected BTC 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 partner's Bitcoin PKH
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to partner's Bitcoin PKH
|
||||
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 Bitcoin PKH
|
||||
byte[] partnerBitcoinPKH = new byte[20];
|
||||
dataByteBuffer.get(partnerBitcoinPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerBitcoinPKH.length); // skip to 32 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 = partnerBitcoinPKH;
|
||||
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[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
|
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||
return Bytes.concat(partnerBitcoinPKH, 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.partnerBitcoinPKH = Arrays.copyOfRange(messageData, 0, 20);
|
||||
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
|
||||
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
|
||||
|
||||
return offerMessageData;
|
||||
}
|
||||
|
||||
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
|
||||
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, 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(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
|
||||
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
|
||||
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
|
||||
System.arraycopy(lockTimeABytes, 0, data, 88, 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -375,7 +375,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
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();
|
||||
@@ -455,7 +455,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 +537,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
171
src/main/java/org/qortal/crosschain/Digibyte.java
Normal file
171
src/main/java/org/qortal/crosschain/Digibyte.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Context;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.libdohj.params.DigibyteMainNetParams;
|
||||
import org.qortal.crosschain.ElectrumX.Server;
|
||||
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class Digibyte extends Bitcoiny {
|
||||
|
||||
public static final String CURRENCY_CODE = "DGB";
|
||||
|
||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(100000); // 0.001 DGB per 1000 bytes
|
||||
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 DGB minimum order, to avoid dust errors
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long MAINNET_FEE = 10000L;
|
||||
private static final long NON_MAINNET_FEE = 10000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||
|
||||
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
|
||||
static {
|
||||
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
|
||||
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
|
||||
}
|
||||
|
||||
public enum DigibyteNet {
|
||||
MAIN {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return DigibyteMainNetParams.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
|
||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20059),
|
||||
new Server("electrum2.cipig.net", ConnectionType.SSL, 20059),
|
||||
new Server("electrum3.cipig.net", ConnectionType.SSL, 20059));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGenesisHash() {
|
||||
return "7497ea1b465eb39f1c8f507bc877078fe016d6fcb6dfad3a64c98dcc6e1e8496";
|
||||
}
|
||||
|
||||
@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 TestNet3Params.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(); // TODO: find testnet servers
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGenesisHash() {
|
||||
return "308ea0711d5763be2995670dd9ca9872753561285a84da1d58be58acaa822252";
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return NON_MAINNET_FEE;
|
||||
}
|
||||
},
|
||||
REGTEST {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return RegTestParams.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(
|
||||
new Server("localhost", ConnectionType.TCP, 50001),
|
||||
new Server("localhost", ConnectionType.SSL, 50002));
|
||||
}
|
||||
|
||||
@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 Digibyte instance;
|
||||
|
||||
private final DigibyteNet digibyteNet;
|
||||
|
||||
// Constructors and instance
|
||||
|
||||
private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
this.digibyteNet = digibyteNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name()));
|
||||
}
|
||||
|
||||
public static synchronized Digibyte getInstance() {
|
||||
if (instance == null) {
|
||||
DigibyteNet digibyteNet = Settings.getInstance().getDigibyteNet();
|
||||
|
||||
BitcoinyBlockchainProvider electrumX = new ElectrumX("Digibyte-" + digibyteNet.name(), digibyteNet.getGenesisHash(), digibyteNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
|
||||
Context bitcoinjContext = new Context(digibyteNet.getParams());
|
||||
|
||||
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Getters & setters
|
||||
|
||||
public static synchronized void resetForTesting() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
@Override
|
||||
public Coin getFeePerKb() {
|
||||
return DEFAULT_FEE_PER_KB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns estimated DGB 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.digibyteNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
}
|
||||
858
src/main/java/org/qortal/crosschain/DigibyteACCTv3.java
Normal file
858
src/main/java/org/qortal/crosschain/DigibyteACCTv3.java
Normal file
@@ -0,0 +1,858 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
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 Digibyte & 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 Digibyte & Qortal 'trade' keys</li>
|
||||
* <li>Alice funds Digibyte 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' Digibyte PKH</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 Digibyte PKH</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 Digibyte trade key and secret-A</li>
|
||||
* <li>P2SH-A DGB funds end up at Digibyte address determined by redeem transaction output(s)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public class DigibyteACCTv3 implements ACCT {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv3.class);
|
||||
|
||||
public static final String NAME = DigibyteACCTv3.class.getSimpleName();
|
||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("e6a7dcd87296fae3ce7d80183bf7660c8e2cb4f8746c6a0421a17148f87a0e1d").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 = 61;
|
||||
/** <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[] partnerDigibytePKH;
|
||||
public byte[] hashOfSecretA;
|
||||
public long lockTimeA;
|
||||
}
|
||||
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDigibytePKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
|
||||
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
|
||||
+ 24 /*partner's Digibyte PKH (padded from 20 to 24)*/
|
||||
+ 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 DigibyteACCTv3 instance;
|
||||
|
||||
private DigibyteACCTv3() {
|
||||
}
|
||||
|
||||
public static synchronized DigibyteACCTv3 getInstance() {
|
||||
if (instance == null)
|
||||
instance = new DigibyteACCTv3();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCodeBytesHash() {
|
||||
return CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModeByteOffset() {
|
||||
return MODE_BYTE_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignBlockchain getBlockchain() {
|
||||
return Digibyte.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 digibytePublicKeyHash 20-byte HASH160 of creator's trade Digibyte public key
|
||||
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
||||
* @param digibyteAmount how much DGB the AT creator is expecting to trade
|
||||
* @param tradeTimeout suggested timeout for entire trade
|
||||
*/
|
||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] digibytePublicKeyHash, long qortAmount, long digibyteAmount, int tradeTimeout) {
|
||||
if (digibytePublicKeyHash.length != 20)
|
||||
throw new IllegalArgumentException("Digibyte public key hash should be 20 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 addrDigibytePublicKeyHash = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrQortAmount = addrCounter++;
|
||||
final int addrDigibyteAmount = 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 addrTradeMessagePartnerDigibytePKHOffset = addrCounter++;
|
||||
final int addrPartnerDigibytePKHPointer = addrCounter++;
|
||||
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 addrPartnerDigibytePKH = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
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));
|
||||
|
||||
// Digibyte public key hash
|
||||
assert dataByteBuffer.position() == addrDigibytePublicKeyHash * MachineState.VALUE_SIZE : "addrDigibytePublicKeyHash incorrect";
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(digibytePublicKeyHash, 32, 0));
|
||||
|
||||
// Redeem Qort amount
|
||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||
dataByteBuffer.putLong(qortAmount);
|
||||
|
||||
// Expected Digibyte amount
|
||||
assert dataByteBuffer.position() == addrDigibyteAmount * MachineState.VALUE_SIZE : "addrDigibyteAmount incorrect";
|
||||
dataByteBuffer.putLong(digibyteAmount);
|
||||
|
||||
// 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 partner's Digibyte PKH
|
||||
assert dataByteBuffer.position() == addrTradeMessagePartnerDigibytePKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDigibytePKHOffset incorrect";
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
// Index into data segment of partner's Digibyte PKH, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerDigibytePKHPointer * MachineState.VALUE_SIZE : "addrPartnerDigibytePKHPointer incorrect";
|
||||
dataByteBuffer.putLong(addrPartnerDigibytePKH);
|
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
||||
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
|
||||
dataByteBuffer.putLong(64L);
|
||||
|
||||
// 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));
|
||||
|
||||
/* NOP - to ensure DIGIBYTE ACCT is unique */
|
||||
codeByteBuffer.put(OpCode.NOP.compile());
|
||||
|
||||
// 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 trade partner's Digibyte public key hash (PKH) from message into B
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDigibytePKHOffset));
|
||||
// Store partner's Digibyte PKH (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDigibytePKHPointer));
|
||||
// Extract AT trade timeout (minutes) (from B4)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, 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 DGB-QORT ACCT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
assert Arrays.equals(Crypto.digest(codeBytes), DigibyteACCTv3.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.DIGIBYTE.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 Digibyte/foreign public key hash
|
||||
tradeData.creatorForeignPKH = new byte[20];
|
||||
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||
|
||||
// We don't use secret-B
|
||||
tradeData.hashOfSecretB = null;
|
||||
|
||||
// Redeem payout
|
||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Expected DGB 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 partner's Digibyte PKH
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to partner's Digibyte PKH
|
||||
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 Digibyte PKH
|
||||
byte[] partnerDigibytePKH = new byte[20];
|
||||
dataByteBuffer.get(partnerDigibytePKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDigibytePKH.length); // skip to 32 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 = partnerDigibytePKH;
|
||||
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[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
|
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||
return Bytes.concat(partnerBitcoinPKH, 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.partnerDigibytePKH = Arrays.copyOfRange(messageData, 0, 20);
|
||||
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
|
||||
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
|
||||
|
||||
return offerMessageData;
|
||||
}
|
||||
|
||||
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
|
||||
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, 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(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
|
||||
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
|
||||
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
|
||||
System.arraycopy(lockTimeABytes, 0, data, 88, 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;
|
||||
}
|
||||
|
||||
}
|
||||
175
src/main/java/org/qortal/crosschain/Ravencoin.java
Normal file
175
src/main/java/org/qortal/crosschain/Ravencoin.java
Normal file
@@ -0,0 +1,175 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Context;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.libdohj.params.RavencoinMainNetParams;
|
||||
import org.qortal.crosschain.ElectrumX.Server;
|
||||
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class Ravencoin extends Bitcoiny {
|
||||
|
||||
public static final String CURRENCY_CODE = "RVN";
|
||||
|
||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(1125000); // 0.01125 RVN per 1000 bytes
|
||||
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 RVN minimum order, to avoid dust errors
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long MAINNET_FEE = 1000000L;
|
||||
private static final long NON_MAINNET_FEE = 1000000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||
|
||||
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
|
||||
static {
|
||||
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
|
||||
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
|
||||
}
|
||||
|
||||
public enum RavencoinNet {
|
||||
MAIN {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return RavencoinMainNetParams.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
|
||||
new Server("aethyn.com", ConnectionType.SSL, 50002),
|
||||
new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
|
||||
new Server("rvn-dashboard.com", ConnectionType.SSL, 50002),
|
||||
new Server("rvn4lyfe.com", ConnectionType.SSL, 50002),
|
||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20051),
|
||||
new Server("electrum2.cipig.net", ConnectionType.SSL, 20051),
|
||||
new Server("electrum3.cipig.net", ConnectionType.SSL, 20051));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGenesisHash() {
|
||||
return "0000006b444bc2f2ffe627be9d9e7e7a0730000870ef6eb6da46c8eae389df90";
|
||||
}
|
||||
|
||||
@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 TestNet3Params.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(); // TODO: find testnet servers
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGenesisHash() {
|
||||
return "000000ecfc5e6324a079542221d00e10362bdc894d56500c414060eea8a3ad5a";
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getP2shFee(Long timestamp) {
|
||||
return NON_MAINNET_FEE;
|
||||
}
|
||||
},
|
||||
REGTEST {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return RegTestParams.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(
|
||||
new Server("localhost", ConnectionType.TCP, 50001),
|
||||
new Server("localhost", ConnectionType.SSL, 50002));
|
||||
}
|
||||
|
||||
@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 Ravencoin instance;
|
||||
|
||||
private final RavencoinNet ravencoinNet;
|
||||
|
||||
// Constructors and instance
|
||||
|
||||
private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||
super(blockchain, bitcoinjContext, currencyCode);
|
||||
this.ravencoinNet = ravencoinNet;
|
||||
|
||||
LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name()));
|
||||
}
|
||||
|
||||
public static synchronized Ravencoin getInstance() {
|
||||
if (instance == null) {
|
||||
RavencoinNet ravencoinNet = Settings.getInstance().getRavencoinNet();
|
||||
|
||||
BitcoinyBlockchainProvider electrumX = new ElectrumX("Ravencoin-" + ravencoinNet.name(), ravencoinNet.getGenesisHash(), ravencoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
|
||||
Context bitcoinjContext = new Context(ravencoinNet.getParams());
|
||||
|
||||
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Getters & setters
|
||||
|
||||
public static synchronized void resetForTesting() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
@Override
|
||||
public Coin getFeePerKb() {
|
||||
return DEFAULT_FEE_PER_KB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMinimumOrderAmount() {
|
||||
return MINIMUM_ORDER_AMOUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns estimated RVN 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.ravencoinNet.getP2shFee(timestamp);
|
||||
}
|
||||
|
||||
}
|
||||
858
src/main/java/org/qortal/crosschain/RavencoinACCTv3.java
Normal file
858
src/main/java/org/qortal/crosschain/RavencoinACCTv3.java
Normal file
@@ -0,0 +1,858 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
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 Ravencoin & 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 Ravencoin & Qortal 'trade' keys</li>
|
||||
* <li>Alice funds Ravencoin 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' Ravencoin PKH</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 Ravencoin PKH</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 Ravencoin trade key and secret-A</li>
|
||||
* <li>P2SH-A RVN funds end up at Ravencoin address determined by redeem transaction output(s)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public class RavencoinACCTv3 implements ACCT {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(RavencoinACCTv3.class);
|
||||
|
||||
public static final String NAME = RavencoinACCTv3.class.getSimpleName();
|
||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("91395fa1ec0dfa35beddb0a7f4cc0a1bede157c38787ddb0af0cf03dfdc10f77").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 = 61;
|
||||
/** <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[] partnerRavencoinPKH;
|
||||
public byte[] hashOfSecretA;
|
||||
public long lockTimeA;
|
||||
}
|
||||
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerRavencoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
|
||||
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
|
||||
+ 24 /*partner's Ravencoin PKH (padded from 20 to 24)*/
|
||||
+ 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 RavencoinACCTv3 instance;
|
||||
|
||||
private RavencoinACCTv3() {
|
||||
}
|
||||
|
||||
public static synchronized RavencoinACCTv3 getInstance() {
|
||||
if (instance == null)
|
||||
instance = new RavencoinACCTv3();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCodeBytesHash() {
|
||||
return CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModeByteOffset() {
|
||||
return MODE_BYTE_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignBlockchain getBlockchain() {
|
||||
return Ravencoin.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 ravencoinPublicKeyHash 20-byte HASH160 of creator's trade Ravencoin public key
|
||||
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
||||
* @param ravencoinAmount how much RVN the AT creator is expecting to trade
|
||||
* @param tradeTimeout suggested timeout for entire trade
|
||||
*/
|
||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] ravencoinPublicKeyHash, long qortAmount, long ravencoinAmount, int tradeTimeout) {
|
||||
if (ravencoinPublicKeyHash.length != 20)
|
||||
throw new IllegalArgumentException("Ravencoin public key hash should be 20 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 addrRavencoinPublicKeyHash = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrQortAmount = addrCounter++;
|
||||
final int addrRavencoinAmount = 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 addrTradeMessagePartnerRavencoinPKHOffset = addrCounter++;
|
||||
final int addrPartnerRavencoinPKHPointer = addrCounter++;
|
||||
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 addrPartnerRavencoinPKH = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
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));
|
||||
|
||||
// Ravencoin public key hash
|
||||
assert dataByteBuffer.position() == addrRavencoinPublicKeyHash * MachineState.VALUE_SIZE : "addrRavencoinPublicKeyHash incorrect";
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(ravencoinPublicKeyHash, 32, 0));
|
||||
|
||||
// Redeem Qort amount
|
||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||
dataByteBuffer.putLong(qortAmount);
|
||||
|
||||
// Expected Ravencoin amount
|
||||
assert dataByteBuffer.position() == addrRavencoinAmount * MachineState.VALUE_SIZE : "addrRavencoinAmount incorrect";
|
||||
dataByteBuffer.putLong(ravencoinAmount);
|
||||
|
||||
// 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 partner's Ravencoin PKH
|
||||
assert dataByteBuffer.position() == addrTradeMessagePartnerRavencoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerRavencoinPKHOffset incorrect";
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
// Index into data segment of partner's Ravencoin PKH, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerRavencoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerRavencoinPKHPointer incorrect";
|
||||
dataByteBuffer.putLong(addrPartnerRavencoinPKH);
|
||||
|
||||
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
||||
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
|
||||
dataByteBuffer.putLong(64L);
|
||||
|
||||
// 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));
|
||||
|
||||
/* NOP - to ensure RAVENCOIN ACCT is unique */
|
||||
codeByteBuffer.put(OpCode.NOP.compile());
|
||||
|
||||
// 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 trade partner's Ravencoin public key hash (PKH) from message into B
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerRavencoinPKHOffset));
|
||||
// Store partner's Ravencoin PKH (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerRavencoinPKHPointer));
|
||||
// Extract AT trade timeout (minutes) (from B4)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, 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 RVN-QORT ACCT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
assert Arrays.equals(Crypto.digest(codeBytes), RavencoinACCTv3.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.RAVENCOIN.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 Ravencoin/foreign public key hash
|
||||
tradeData.creatorForeignPKH = new byte[20];
|
||||
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
||||
|
||||
// We don't use secret-B
|
||||
tradeData.hashOfSecretB = null;
|
||||
|
||||
// Redeem payout
|
||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||
|
||||
// Expected RVN 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 partner's Ravencoin PKH
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to partner's Ravencoin PKH
|
||||
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 Ravencoin PKH
|
||||
byte[] partnerRavencoinPKH = new byte[20];
|
||||
dataByteBuffer.get(partnerRavencoinPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerRavencoinPKH.length); // skip to 32 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 = partnerRavencoinPKH;
|
||||
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[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
|
||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||
return Bytes.concat(partnerBitcoinPKH, 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.partnerRavencoinPKH = Arrays.copyOfRange(messageData, 0, 20);
|
||||
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
|
||||
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
|
||||
|
||||
return offerMessageData;
|
||||
}
|
||||
|
||||
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
|
||||
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, 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(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
|
||||
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
|
||||
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
|
||||
System.arraycopy(lockTimeABytes, 0, data, 88, 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@ 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;
|
||||
@@ -74,7 +74,7 @@ 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) {
|
||||
this.txHash = txHash;
|
||||
this.timestamp = timestamp;
|
||||
this.totalAmount = totalAmount;
|
||||
@@ -87,7 +87,7 @@ public class SimpleTransaction {
|
||||
return txHash;
|
||||
}
|
||||
|
||||
public Integer getTimestamp() {
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import org.qortal.utils.Triple;
|
||||
public enum SupportedBlockchain {
|
||||
|
||||
BITCOIN(Arrays.asList(
|
||||
Triple.valueOf(BitcoinACCTv1.NAME, BitcoinACCTv1.CODE_BYTES_HASH, BitcoinACCTv1::getInstance)
|
||||
// Could add improved BitcoinACCTv2 here in the future
|
||||
Triple.valueOf(BitcoinACCTv1.NAME, BitcoinACCTv1.CODE_BYTES_HASH, BitcoinACCTv1::getInstance),
|
||||
Triple.valueOf(BitcoinACCTv3.NAME, BitcoinACCTv3.CODE_BYTES_HASH, BitcoinACCTv3::getInstance)
|
||||
)) {
|
||||
@Override
|
||||
public ForeignBlockchain getInstance() {
|
||||
@@ -23,7 +23,7 @@ public enum SupportedBlockchain {
|
||||
|
||||
@Override
|
||||
public ACCT getLatestAcct() {
|
||||
return BitcoinACCTv1.getInstance();
|
||||
return BitcoinACCTv3.getInstance();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -57,6 +57,34 @@ public enum SupportedBlockchain {
|
||||
public ACCT getLatestAcct() {
|
||||
return DogecoinACCTv3.getInstance();
|
||||
}
|
||||
},
|
||||
|
||||
DIGIBYTE(Arrays.asList(
|
||||
Triple.valueOf(DigibyteACCTv3.NAME, DigibyteACCTv3.CODE_BYTES_HASH, DigibyteACCTv3::getInstance)
|
||||
)) {
|
||||
@Override
|
||||
public ForeignBlockchain getInstance() {
|
||||
return Digibyte.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ACCT getLatestAcct() {
|
||||
return DigibyteACCTv3.getInstance();
|
||||
}
|
||||
},
|
||||
|
||||
RAVENCOIN(Arrays.asList(
|
||||
Triple.valueOf(RavencoinACCTv3.NAME, RavencoinACCTv3.CODE_BYTES_HASH, RavencoinACCTv3::getInstance)
|
||||
)) {
|
||||
@Override
|
||||
public ForeignBlockchain getInstance() {
|
||||
return Ravencoin.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ACCT getLatestAcct() {
|
||||
return RavencoinACCTv3.getInstance();
|
||||
}
|
||||
};
|
||||
|
||||
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.List;
|
||||
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.SwingWorker;
|
||||
import javax.swing.event.PopupMenuEvent;
|
||||
@@ -178,6 +179,14 @@ public class SysTray {
|
||||
menu.add(syncTime);
|
||||
}
|
||||
|
||||
JMenuItem about = new JMenuItem(Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"));
|
||||
about.addActionListener(actionEvent -> {
|
||||
destroyHiddenDialog();
|
||||
|
||||
JOptionPane.showMessageDialog(null,"Qortal Core\n" + Translator.INSTANCE.translate("SysTray", "BUILD_VERSION") + ":\n" + Controller.getInstance().getVersionStringWithoutPrefix(),"Qortal Core",1);
|
||||
});
|
||||
menu.add(about);
|
||||
|
||||
JMenuItem exit = new JMenuItem(Translator.INSTANCE.translate("SysTray", "EXIT"));
|
||||
exit.addActionListener(actionEvent -> {
|
||||
destroyHiddenDialog();
|
||||
|
||||
@@ -195,7 +195,7 @@ public class Name {
|
||||
this.repository.getNameRepository().save(this.nameData);
|
||||
}
|
||||
|
||||
public void buy(BuyNameTransactionData buyNameTransactionData) throws DataException {
|
||||
public void buy(BuyNameTransactionData buyNameTransactionData, boolean modifyBalances) throws DataException {
|
||||
// Save previous name-changing reference in this transaction's data
|
||||
// Caller is expected to save
|
||||
buyNameTransactionData.setNameReference(this.nameData.getReference());
|
||||
@@ -203,15 +203,20 @@ public class Name {
|
||||
// Mark not for-sale but leave price in case we want to orphan
|
||||
this.nameData.setIsForSale(false);
|
||||
|
||||
// Update seller's balance
|
||||
Account seller = new Account(this.repository, this.nameData.getOwner());
|
||||
seller.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount());
|
||||
if (modifyBalances) {
|
||||
// Update seller's balance
|
||||
Account seller = new Account(this.repository, this.nameData.getOwner());
|
||||
seller.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount());
|
||||
}
|
||||
|
||||
// Set new owner
|
||||
Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey());
|
||||
this.nameData.setOwner(buyer.getAddress());
|
||||
// Update buyer's balance
|
||||
buyer.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount());
|
||||
|
||||
if (modifyBalances) {
|
||||
// Update buyer's balance
|
||||
buyer.modifyAssetBalance(Asset.QORT, -buyNameTransactionData.getAmount());
|
||||
}
|
||||
|
||||
// Set name-changing reference to this transaction
|
||||
this.nameData.setReference(buyNameTransactionData.getSignature());
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.network.message.ChallengeMessage;
|
||||
import org.qortal.network.message.HelloMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.Message.MessageType;
|
||||
import org.qortal.network.message.MessageType;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.network.message.ResponseMessage;
|
||||
import org.qortal.utils.DaemonThreadFactory;
|
||||
|
||||
@@ -8,11 +8,13 @@ import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.message.*;
|
||||
import org.qortal.network.task.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -32,6 +34,7 @@ import java.nio.channels.*;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
@@ -41,9 +44,8 @@ import java.util.stream.Collectors;
|
||||
// For managing peers
|
||||
public class Network {
|
||||
private static final Logger LOGGER = LogManager.getLogger(Network.class);
|
||||
private static Network instance;
|
||||
|
||||
private static final int LISTEN_BACKLOG = 10;
|
||||
private static final int LISTEN_BACKLOG = 5;
|
||||
/**
|
||||
* How long before retrying after a connection failure, in milliseconds.
|
||||
*/
|
||||
@@ -122,14 +124,8 @@ public class Network {
|
||||
private final ExecuteProduceConsume networkEPC;
|
||||
private Selector channelSelector;
|
||||
private ServerSocketChannel serverChannel;
|
||||
private Iterator<SelectionKey> channelIterator = null;
|
||||
|
||||
// volatile because value is updated inside any one of the EPC threads
|
||||
private volatile long nextConnectTaskTimestamp = 0L; // ms - try first connect once NTP syncs
|
||||
|
||||
private final ExecutorService broadcastExecutor = Executors.newCachedThreadPool();
|
||||
// volatile because value is updated inside any one of the EPC threads
|
||||
private volatile long nextBroadcastTimestamp = 0L; // ms - try first broadcast once NTP syncs
|
||||
private SelectionKey serverSelectionKey;
|
||||
private final Set<SelectableChannel> channelsPendingWrite = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final Lock mergePeersLock = new ReentrantLock();
|
||||
|
||||
@@ -137,6 +133,8 @@ public class Network {
|
||||
private String ourExternalIpAddress = null;
|
||||
private int ourExternalPort = Settings.getInstance().getListenPort();
|
||||
|
||||
private volatile boolean isShuttingDown = false;
|
||||
|
||||
// Constructors
|
||||
|
||||
private Network() {
|
||||
@@ -170,7 +168,7 @@ public class Network {
|
||||
serverChannel.configureBlocking(false);
|
||||
serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
|
||||
serverChannel.bind(endpoint, LISTEN_BACKLOG);
|
||||
serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
|
||||
serverSelectionKey = serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
|
||||
} catch (UnknownHostException e) {
|
||||
LOGGER.error("Can't bind listen socket to address {}", Settings.getInstance().getBindAddress());
|
||||
throw new IOException("Can't bind listen socket to address", e);
|
||||
@@ -180,7 +178,8 @@ public class Network {
|
||||
}
|
||||
|
||||
// Load all known peers from repository
|
||||
synchronized (this.allKnownPeers) { List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
|
||||
synchronized (this.allKnownPeers) {
|
||||
List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
|
||||
if (fixedNetwork != null && !fixedNetwork.isEmpty()) {
|
||||
Long addedWhen = NTP.getTime();
|
||||
String addedBy = "fixedNetwork";
|
||||
@@ -214,12 +213,16 @@ public class Network {
|
||||
|
||||
// Getters / setters
|
||||
|
||||
public static synchronized Network getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new Network();
|
||||
}
|
||||
private static class SingletonContainer {
|
||||
private static final Network INSTANCE = new Network();
|
||||
}
|
||||
|
||||
return instance;
|
||||
public static Network getInstance() {
|
||||
return SingletonContainer.INSTANCE;
|
||||
}
|
||||
|
||||
public int getMaxPeers() {
|
||||
return this.maxPeers;
|
||||
}
|
||||
|
||||
public byte[] getMessageMagic() {
|
||||
@@ -257,6 +260,18 @@ public class Network {
|
||||
return this.immutableConnectedPeers;
|
||||
}
|
||||
|
||||
public List<Peer> getImmutableConnectedDataPeers() {
|
||||
return this.getImmutableConnectedPeers().stream()
|
||||
.filter(p -> p.isDataPeer())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<Peer> getImmutableConnectedNonDataPeers() {
|
||||
return this.getImmutableConnectedPeers().stream()
|
||||
.filter(p -> !p.isDataPeer())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void addConnectedPeer(Peer peer) {
|
||||
this.connectedPeers.add(peer); // thread safe thanks to synchronized list
|
||||
this.immutableConnectedPeers = List.copyOf(this.connectedPeers); // also thread safe thanks to synchronized collection's toArray() being fed to List.of(array)
|
||||
@@ -323,6 +338,7 @@ public class Network {
|
||||
// Add this signature to the list of pending requests for this peer
|
||||
LOGGER.info("Making connection to peer {} to request files for signature {}...", peerAddressString, Base58.encode(signature));
|
||||
Peer peer = new Peer(peerData);
|
||||
peer.setIsDataPeer(true);
|
||||
peer.addPendingSignatureRequest(signature);
|
||||
return this.connectPeer(peer);
|
||||
// If connection (and handshake) is successful, data will automatically be requested
|
||||
@@ -453,6 +469,13 @@ 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
|
||||
|
||||
private Iterator<SelectionKey> channelIterator = null;
|
||||
|
||||
NetworkProcessor(ExecutorService executor) {
|
||||
super(executor);
|
||||
}
|
||||
@@ -494,43 +517,23 @@ public class Network {
|
||||
}
|
||||
|
||||
private Task maybeProducePeerMessageTask() {
|
||||
for (Peer peer : getImmutableConnectedPeers()) {
|
||||
Task peerTask = peer.getMessageTask();
|
||||
if (peerTask != null) {
|
||||
return peerTask;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return getImmutableConnectedPeers().stream()
|
||||
.map(Peer::getMessageTask)
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Task maybeProducePeerPingTask(Long now) {
|
||||
// Ask connected peers whether they need a ping
|
||||
for (Peer peer : getImmutableHandshakedPeers()) {
|
||||
Task peerTask = peer.getPingTask(now);
|
||||
if (peerTask != null) {
|
||||
return peerTask;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class PeerConnectTask implements ExecuteProduceConsume.Task {
|
||||
private final Peer peer;
|
||||
|
||||
PeerConnectTask(Peer peer) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform() throws InterruptedException {
|
||||
connectPeer(peer);
|
||||
}
|
||||
return getImmutableHandshakedPeers().stream()
|
||||
.map(peer -> peer.getPingTask(now))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Task maybeProduceConnectPeerTask(Long now) throws InterruptedException {
|
||||
if (now == null || now < nextConnectTaskTimestamp) {
|
||||
if (now == null || now < nextConnectTaskTimestamp.get()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -538,7 +541,7 @@ public class Network {
|
||||
return null;
|
||||
}
|
||||
|
||||
nextConnectTaskTimestamp = now + 1000L;
|
||||
nextConnectTaskTimestamp.set(now + 1000L);
|
||||
|
||||
Peer targetPeer = getConnectablePeer(now);
|
||||
if (targetPeer == null) {
|
||||
@@ -550,66 +553,15 @@ public class Network {
|
||||
}
|
||||
|
||||
private Task maybeProduceBroadcastTask(Long now) {
|
||||
if (now == null || now < nextBroadcastTimestamp) {
|
||||
if (now == null || now < nextBroadcastTimestamp.get()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
nextBroadcastTimestamp = now + BROADCAST_INTERVAL;
|
||||
return () -> Controller.getInstance().doNetworkBroadcast();
|
||||
}
|
||||
|
||||
class ChannelTask implements ExecuteProduceConsume.Task {
|
||||
private final SelectionKey selectionKey;
|
||||
|
||||
ChannelTask(SelectionKey selectionKey) {
|
||||
this.selectionKey = selectionKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform() throws InterruptedException {
|
||||
try {
|
||||
LOGGER.trace("Thread {} has pending channel: {}, with ops {}",
|
||||
Thread.currentThread().getId(), selectionKey.channel(), selectionKey.readyOps());
|
||||
|
||||
// process pending channel task
|
||||
if (selectionKey.isReadable()) {
|
||||
connectionRead((SocketChannel) selectionKey.channel());
|
||||
} else if (selectionKey.isAcceptable()) {
|
||||
acceptConnection((ServerSocketChannel) selectionKey.channel());
|
||||
}
|
||||
|
||||
LOGGER.trace("Thread {} processed channel: {}",
|
||||
Thread.currentThread().getId(), selectionKey.channel());
|
||||
} catch (CancelledKeyException e) {
|
||||
LOGGER.trace("Thread {} encountered cancelled channel: {}",
|
||||
Thread.currentThread().getId(), selectionKey.channel());
|
||||
}
|
||||
}
|
||||
|
||||
private void connectionRead(SocketChannel socketChannel) {
|
||||
Peer peer = getPeerFromChannel(socketChannel);
|
||||
if (peer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
peer.readChannel();
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage() != null && e.getMessage().toLowerCase().contains("connection reset")) {
|
||||
peer.disconnect("Connection reset");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.trace("[{}] Network thread {} encountered I/O error: {}", peer.getPeerConnectionId(),
|
||||
Thread.currentThread().getId(), e.getMessage(), e);
|
||||
peer.disconnect("I/O error");
|
||||
}
|
||||
}
|
||||
nextBroadcastTimestamp.set(now + BROADCAST_INTERVAL);
|
||||
return new BroadcastTask();
|
||||
}
|
||||
|
||||
private Task maybeProduceChannelTask(boolean canBlock) throws InterruptedException {
|
||||
final SelectionKey nextSelectionKey;
|
||||
|
||||
// Synchronization here to enforce thread-safety on channelIterator
|
||||
synchronized (channelSelector) {
|
||||
// anything to do?
|
||||
@@ -630,91 +582,73 @@ public class Network {
|
||||
}
|
||||
|
||||
channelIterator = channelSelector.selectedKeys().iterator();
|
||||
LOGGER.trace("Thread {}, after {} select, channelIterator now {}",
|
||||
Thread.currentThread().getId(),
|
||||
canBlock ? "blocking": "non-blocking",
|
||||
channelIterator);
|
||||
}
|
||||
|
||||
if (channelIterator.hasNext()) {
|
||||
nextSelectionKey = channelIterator.next();
|
||||
channelIterator.remove();
|
||||
} else {
|
||||
nextSelectionKey = null;
|
||||
if (!channelIterator.hasNext()) {
|
||||
channelIterator = null; // Nothing to do so reset iterator to cause new select
|
||||
|
||||
LOGGER.trace("Thread {}, channelIterator now null", Thread.currentThread().getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
LOGGER.trace("Thread {}, nextSelectionKey {}, channelIterator now {}",
|
||||
Thread.currentThread().getId(), nextSelectionKey, channelIterator);
|
||||
}
|
||||
final SelectionKey nextSelectionKey = channelIterator.next();
|
||||
channelIterator.remove();
|
||||
|
||||
if (nextSelectionKey == null) {
|
||||
return null;
|
||||
}
|
||||
// Just in case underlying socket channel already closed elsewhere, etc.
|
||||
if (!nextSelectionKey.isValid())
|
||||
return null;
|
||||
|
||||
return new ChannelTask(nextSelectionKey);
|
||||
}
|
||||
}
|
||||
LOGGER.trace("Thread {}, nextSelectionKey {}", Thread.currentThread().getId(), nextSelectionKey);
|
||||
|
||||
private void acceptConnection(ServerSocketChannel serverSocketChannel) throws InterruptedException {
|
||||
SocketChannel socketChannel;
|
||||
SelectableChannel socketChannel = nextSelectionKey.channel();
|
||||
|
||||
try {
|
||||
socketChannel = serverSocketChannel.accept();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No connection actually accepted?
|
||||
if (socketChannel == null) {
|
||||
return;
|
||||
}
|
||||
PeerAddress address = PeerAddress.fromSocket(socketChannel.socket());
|
||||
List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
|
||||
if (fixedNetwork != null && !fixedNetwork.isEmpty() && ipNotInFixedList(address, fixedNetwork)) {
|
||||
try {
|
||||
LOGGER.debug("Connection discarded from peer {} as not in the fixed network list", address);
|
||||
socketChannel.close();
|
||||
} catch (IOException e) {
|
||||
// IGNORE
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final Long now = NTP.getTime();
|
||||
Peer newPeer;
|
||||
|
||||
try {
|
||||
if (now == null) {
|
||||
LOGGER.debug("Connection discarded from peer {} due to lack of NTP sync", address);
|
||||
socketChannel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (getImmutableConnectedPeers().size() >= maxPeers) {
|
||||
// We have enough peers
|
||||
LOGGER.debug("Connection discarded from peer {} because the server is full", address);
|
||||
socketChannel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Connection accepted from peer {}", address);
|
||||
|
||||
newPeer = new Peer(socketChannel, channelSelector);
|
||||
this.addConnectedPeer(newPeer);
|
||||
|
||||
} catch (IOException e) {
|
||||
if (socketChannel.isOpen()) {
|
||||
try {
|
||||
LOGGER.debug("Connection failed from peer {} while connecting/closing", address);
|
||||
socketChannel.close();
|
||||
} catch (IOException ce) {
|
||||
// Couldn't close?
|
||||
if (nextSelectionKey.isReadable()) {
|
||||
clearInterestOps(nextSelectionKey, SelectionKey.OP_READ);
|
||||
Peer peer = getPeerFromChannel((SocketChannel) socketChannel);
|
||||
if (peer == null)
|
||||
return null;
|
||||
|
||||
return new ChannelReadTask((SocketChannel) socketChannel, peer);
|
||||
}
|
||||
|
||||
if (nextSelectionKey.isWritable()) {
|
||||
clearInterestOps(nextSelectionKey, SelectionKey.OP_WRITE);
|
||||
Peer peer = getPeerFromChannel((SocketChannel) socketChannel);
|
||||
if (peer == null)
|
||||
return null;
|
||||
|
||||
// Any thread that queues a message to send can set OP_WRITE,
|
||||
// but we only allow one pending/active ChannelWriteTask per Peer
|
||||
if (!channelsPendingWrite.add(socketChannel))
|
||||
return null;
|
||||
|
||||
return new ChannelWriteTask((SocketChannel) socketChannel, peer);
|
||||
}
|
||||
|
||||
if (nextSelectionKey.isAcceptable()) {
|
||||
clearInterestOps(nextSelectionKey, SelectionKey.OP_ACCEPT);
|
||||
return new ChannelAcceptTask((ServerSocketChannel) socketChannel);
|
||||
}
|
||||
} catch (CancelledKeyException e) {
|
||||
/*
|
||||
* Sometimes nextSelectionKey is cancelled / becomes invalid between the isValid() test at line 586
|
||||
* and later calls to isReadable() / isWritable() / isAcceptable() which themselves call isValid()!
|
||||
* Those isXXXable() calls could throw CancelledKeyException, so we catch it here and return null.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.onPeerReady(newPeer);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ipNotInFixedList(PeerAddress address, List<String> fixedNetwork) {
|
||||
public boolean ipNotInFixedList(PeerAddress address, List<String> fixedNetwork) {
|
||||
for (String ipAddress : fixedNetwork) {
|
||||
String[] bits = ipAddress.split(":");
|
||||
if (bits.length >= 1 && bits.length <= 2 && address.getHost().equals(bits[0])) {
|
||||
@@ -750,8 +684,9 @@ public class Network {
|
||||
peers.removeIf(isConnectedPeer);
|
||||
|
||||
// Don't consider already connected peers (resolved address match)
|
||||
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
|
||||
peers.removeIf(isResolvedAsConnectedPeer);
|
||||
// Disabled because this might be too slow if we end up waiting a long time for hostnames to resolve via DNS
|
||||
// Which is ok because duplicate connections to the same peer are handled during handshaking
|
||||
// peers.removeIf(isResolvedAsConnectedPeer);
|
||||
|
||||
this.checkLongestConnection(now);
|
||||
|
||||
@@ -766,6 +701,7 @@ public class Network {
|
||||
// Pick candidate
|
||||
PeerData peerData = peers.get(peerIndex);
|
||||
Peer newPeer = new Peer(peerData);
|
||||
newPeer.setIsDataPeer(false);
|
||||
|
||||
// Update connection attempt info
|
||||
peerData.setLastAttempted(now);
|
||||
@@ -781,8 +717,12 @@ public class Network {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean connectPeer(Peer newPeer) throws InterruptedException {
|
||||
SocketChannel socketChannel = newPeer.connect(this.channelSelector);
|
||||
public boolean connectPeer(Peer newPeer) throws InterruptedException {
|
||||
// Also checked before creating PeerConnectTask
|
||||
if (getImmutableOutboundHandshakedPeers().size() >= minOutboundPeers)
|
||||
return false;
|
||||
|
||||
SocketChannel socketChannel = newPeer.connect();
|
||||
if (socketChannel == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -797,7 +737,7 @@ public class Network {
|
||||
return true;
|
||||
}
|
||||
|
||||
private Peer getPeerFromChannel(SocketChannel socketChannel) {
|
||||
public Peer getPeerFromChannel(SocketChannel socketChannel) {
|
||||
for (Peer peer : this.getImmutableConnectedPeers()) {
|
||||
if (peer.getSocketChannel() == socketChannel) {
|
||||
return peer;
|
||||
@@ -830,7 +770,74 @@ public class Network {
|
||||
nextDisconnectionCheck = now + DISCONNECTION_CHECK_INTERVAL;
|
||||
}
|
||||
|
||||
// Peer callbacks
|
||||
// SocketChannel interest-ops manipulations
|
||||
|
||||
private static final String[] OP_NAMES = new String[SelectionKey.OP_ACCEPT * 2];
|
||||
static {
|
||||
for (int i = 0; i < OP_NAMES.length; i++) {
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
|
||||
if ((i & SelectionKey.OP_READ) != 0) joiner.add("OP_READ");
|
||||
if ((i & SelectionKey.OP_WRITE) != 0) joiner.add("OP_WRITE");
|
||||
if ((i & SelectionKey.OP_CONNECT) != 0) joiner.add("OP_CONNECT");
|
||||
if ((i & SelectionKey.OP_ACCEPT) != 0) joiner.add("OP_ACCEPT");
|
||||
|
||||
OP_NAMES[i] = joiner.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearInterestOps(SelectableChannel socketChannel, int interestOps) {
|
||||
SelectionKey selectionKey = socketChannel.keyFor(channelSelector);
|
||||
if (selectionKey == null)
|
||||
return;
|
||||
|
||||
clearInterestOps(selectionKey, interestOps);
|
||||
}
|
||||
|
||||
private void clearInterestOps(SelectionKey selectionKey, int interestOps) {
|
||||
if (!selectionKey.channel().isOpen())
|
||||
return;
|
||||
|
||||
LOGGER.trace("Thread {} clearing {} interest-ops on channel: {}",
|
||||
Thread.currentThread().getId(),
|
||||
OP_NAMES[interestOps],
|
||||
selectionKey.channel());
|
||||
|
||||
selectionKey.interestOpsAnd(~interestOps);
|
||||
}
|
||||
|
||||
public void setInterestOps(SelectableChannel socketChannel, int interestOps) {
|
||||
SelectionKey selectionKey = socketChannel.keyFor(channelSelector);
|
||||
if (selectionKey == null) {
|
||||
try {
|
||||
selectionKey = socketChannel.register(this.channelSelector, interestOps);
|
||||
} catch (ClosedChannelException e) {
|
||||
// Channel already closed so ignore
|
||||
return;
|
||||
}
|
||||
// Fall-through to allow logging
|
||||
}
|
||||
|
||||
setInterestOps(selectionKey, interestOps);
|
||||
}
|
||||
|
||||
private void setInterestOps(SelectionKey selectionKey, int interestOps) {
|
||||
if (!selectionKey.channel().isOpen())
|
||||
return;
|
||||
|
||||
LOGGER.trace("Thread {} setting {} interest-ops on channel: {}",
|
||||
Thread.currentThread().getId(),
|
||||
OP_NAMES[interestOps],
|
||||
selectionKey.channel());
|
||||
|
||||
selectionKey.interestOpsOr(interestOps);
|
||||
}
|
||||
|
||||
// Peer / Task callbacks
|
||||
|
||||
public void notifyChannelNotWriting(SelectableChannel socketChannel) {
|
||||
this.channelsPendingWrite.remove(socketChannel);
|
||||
}
|
||||
|
||||
protected void wakeupChannelSelector() {
|
||||
this.channelSelector.wakeup();
|
||||
@@ -856,8 +863,6 @@ public class Network {
|
||||
}
|
||||
|
||||
public void onDisconnect(Peer peer) {
|
||||
// Notify Controller
|
||||
Controller.getInstance().onPeerDisconnect(peer);
|
||||
if (peer.getConnectionEstablishedTime() > 0L) {
|
||||
LOGGER.debug("[{}] Disconnected from peer {}", peer.getPeerConnectionId(), peer);
|
||||
} else {
|
||||
@@ -865,6 +870,25 @@ public class Network {
|
||||
}
|
||||
|
||||
this.removeConnectedPeer(peer);
|
||||
this.channelsPendingWrite.remove(peer.getSocketChannel());
|
||||
|
||||
if (this.isShuttingDown)
|
||||
// No need to do any further processing, like re-enabling listen socket or notifying Controller
|
||||
return;
|
||||
|
||||
if (getImmutableConnectedPeers().size() < maxPeers - 1
|
||||
&& serverSelectionKey.isValid()
|
||||
&& (serverSelectionKey.interestOps() & SelectionKey.OP_ACCEPT) == 0) {
|
||||
try {
|
||||
LOGGER.debug("Re-enabling accepting incoming connections because the server is not longer full");
|
||||
setInterestOps(serverSelectionKey, SelectionKey.OP_ACCEPT);
|
||||
} catch (CancelledKeyException e) {
|
||||
LOGGER.error("Failed to re-enable accepting of incoming connections: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Notify Controller
|
||||
Controller.getInstance().onPeerDisconnect(peer);
|
||||
}
|
||||
|
||||
public void peerMisbehaved(Peer peer) {
|
||||
@@ -1062,11 +1086,13 @@ public class Network {
|
||||
// (If inbound sent anything here, it's possible it could be processed out-of-order with handshake message).
|
||||
|
||||
if (peer.isOutbound()) {
|
||||
// Send our height
|
||||
Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
|
||||
if (!peer.sendMessage(heightMessage)) {
|
||||
peer.disconnect("failed to send height/info");
|
||||
return;
|
||||
if (!Settings.getInstance().isLite()) {
|
||||
// Send our height
|
||||
Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
|
||||
if (!peer.sendMessage(heightMessage)) {
|
||||
peer.disconnect("failed to send height/info");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send our peers list
|
||||
@@ -1302,8 +1328,9 @@ public class Network {
|
||||
try {
|
||||
InetSocketAddress knownAddress = peerAddress.toSocketAddress();
|
||||
|
||||
List<Peer> peers = this.getImmutableConnectedPeers();
|
||||
peers.removeIf(peer -> !Peer.addressEquals(knownAddress, peer.getResolvedAddress()));
|
||||
List<Peer> peers = this.getImmutableConnectedPeers().stream()
|
||||
.filter(peer -> Peer.addressEquals(knownAddress, peer.getResolvedAddress()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Peer peer : peers) {
|
||||
peer.disconnect("to be forgotten");
|
||||
@@ -1348,17 +1375,26 @@ public class Network {
|
||||
// We attempted to connect within the last day
|
||||
// but we last managed to connect over a week ago.
|
||||
Predicate<PeerData> isNotOldPeer = peerData -> {
|
||||
if (peerData.getLastAttempted() == null
|
||||
|| peerData.getLastAttempted() < now - OLD_PEER_ATTEMPTED_PERIOD) {
|
||||
|
||||
// First check if there was a connection attempt within the last day
|
||||
if (peerData.getLastAttempted() != null
|
||||
&& peerData.getLastAttempted() > now - OLD_PEER_ATTEMPTED_PERIOD) {
|
||||
|
||||
// There was, so now check if we had a successful connection in the last 7 days
|
||||
if (peerData.getLastConnected() != null
|
||||
&& peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD) {
|
||||
|
||||
// We did, so this is NOT an 'old' peer
|
||||
return true;
|
||||
}
|
||||
|
||||
// Last successful connection was more than 1 week ago - this is an 'old' peer
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// Best to wait until we have a connection attempt - assume not an 'old' peer until then
|
||||
return true;
|
||||
}
|
||||
|
||||
if (peerData.getLastConnected() == null
|
||||
|| peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Disregard peers that are NOT 'old'
|
||||
@@ -1461,54 +1497,27 @@ public class Network {
|
||||
}
|
||||
|
||||
public void broadcast(Function<Peer, Message> peerMessageBuilder) {
|
||||
class Broadcaster implements Runnable {
|
||||
private final Random random = new Random();
|
||||
for (Peer peer : getImmutableHandshakedPeers()) {
|
||||
if (this.isShuttingDown)
|
||||
return;
|
||||
|
||||
private List<Peer> targetPeers;
|
||||
private Function<Peer, Message> peerMessageBuilder;
|
||||
Message message = peerMessageBuilder.apply(peer);
|
||||
|
||||
Broadcaster(List<Peer> targetPeers, Function<Peer, Message> peerMessageBuilder) {
|
||||
this.targetPeers = targetPeers;
|
||||
this.peerMessageBuilder = peerMessageBuilder;
|
||||
if (message == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Network Broadcast");
|
||||
|
||||
for (Peer peer : targetPeers) {
|
||||
// Very short sleep to reduce strain, improve multi-threading and catch interrupts
|
||||
try {
|
||||
Thread.sleep(random.nextInt(20) + 20L);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
Message message = peerMessageBuilder.apply(peer);
|
||||
|
||||
if (message == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!peer.sendMessage(message)) {
|
||||
peer.disconnect("failed to broadcast message");
|
||||
}
|
||||
}
|
||||
|
||||
Thread.currentThread().setName("Network Broadcast (dormant)");
|
||||
if (!peer.sendMessage(message)) {
|
||||
peer.disconnect("failed to broadcast message");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
broadcastExecutor.execute(new Broadcaster(this.getImmutableHandshakedPeers(), peerMessageBuilder));
|
||||
} catch (RejectedExecutionException e) {
|
||||
// Can't execute - probably because we're shutting down, so ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown
|
||||
|
||||
public void shutdown() {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
// Close listen socket to prevent more incoming connections
|
||||
if (this.serverChannel.isOpen()) {
|
||||
try {
|
||||
@@ -1527,16 +1536,6 @@ public class Network {
|
||||
LOGGER.warn("Interrupted while waiting for networking threads to terminate");
|
||||
}
|
||||
|
||||
// Stop broadcasts
|
||||
this.broadcastExecutor.shutdownNow();
|
||||
try {
|
||||
if (!this.broadcastExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS)) {
|
||||
LOGGER.warn("Broadcast threads failed to terminate");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.warn("Interrupted while waiting for broadcast threads failed to terminate");
|
||||
}
|
||||
|
||||
// Close all peer connections
|
||||
for (Peer peer : this.getImmutableConnectedPeers()) {
|
||||
peer.shutdown();
|
||||
|
||||
@@ -11,25 +11,22 @@ import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.message.ChallengeMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.Message.MessageException;
|
||||
import org.qortal.network.message.Message.MessageType;
|
||||
import org.qortal.network.message.PingMessage;
|
||||
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;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.ExecuteProduceConsume.Task;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -48,9 +45,9 @@ public class Peer {
|
||||
private static final int RESPONSE_TIMEOUT = 3000; // ms
|
||||
|
||||
/**
|
||||
* Maximum time to wait for a peer to respond with blocks (ms)
|
||||
* Maximum time to wait for a message to be added to sendQueue (ms)
|
||||
*/
|
||||
public static final int FETCH_BLOCKS_TIMEOUT = 10000;
|
||||
private static final int QUEUE_TIMEOUT = 1000; // ms
|
||||
|
||||
/**
|
||||
* Interval between PING messages to a peer. (ms)
|
||||
@@ -68,13 +65,22 @@ public class Peer {
|
||||
*/
|
||||
private boolean isLocal;
|
||||
|
||||
/**
|
||||
* True if connected for the purposes of transfering specific QDN data
|
||||
*/
|
||||
private boolean isDataPeer;
|
||||
|
||||
private final UUID peerConnectionId = UUID.randomUUID();
|
||||
private final Object byteBufferLock = new Object();
|
||||
private ByteBuffer byteBuffer;
|
||||
|
||||
private Map<Integer, BlockingQueue<Message>> replyQueues;
|
||||
private LinkedBlockingQueue<Message> pendingMessages;
|
||||
|
||||
private TransferQueue<Message> sendQueue;
|
||||
private ByteBuffer outputBuffer;
|
||||
private String outputMessageType;
|
||||
private int outputMessageId;
|
||||
|
||||
/**
|
||||
* True if we created connection to peer, false if we accepted incoming connection from peer.
|
||||
*/
|
||||
@@ -98,7 +104,7 @@ public class Peer {
|
||||
/**
|
||||
* When last PING message was sent, or null if pings not started yet.
|
||||
*/
|
||||
private Long lastPingSent;
|
||||
private Long lastPingSent = null;
|
||||
|
||||
byte[] ourChallenge;
|
||||
|
||||
@@ -160,10 +166,10 @@ public class Peer {
|
||||
/**
|
||||
* Construct Peer using existing, connected socket
|
||||
*/
|
||||
public Peer(SocketChannel socketChannel, Selector channelSelector) throws IOException {
|
||||
public Peer(SocketChannel socketChannel) throws IOException {
|
||||
this.isOutbound = false;
|
||||
this.socketChannel = socketChannel;
|
||||
sharedSetup(channelSelector);
|
||||
sharedSetup();
|
||||
|
||||
this.resolvedAddress = ((InetSocketAddress) socketChannel.socket().getRemoteSocketAddress());
|
||||
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
|
||||
@@ -194,6 +200,14 @@ public class Peer {
|
||||
return this.isOutbound;
|
||||
}
|
||||
|
||||
public boolean isDataPeer() {
|
||||
return isDataPeer;
|
||||
}
|
||||
|
||||
public void setIsDataPeer(boolean isDataPeer) {
|
||||
this.isDataPeer = isDataPeer;
|
||||
}
|
||||
|
||||
public Handshake getHandshakeStatus() {
|
||||
synchronized (this.handshakingLock) {
|
||||
return this.handshakeStatus;
|
||||
@@ -211,6 +225,11 @@ public class Peer {
|
||||
}
|
||||
|
||||
private void generateRandomMaxConnectionAge() {
|
||||
if (this.maxConnectionAge > 0L) {
|
||||
// Already generated, so we don't want to overwrite the existing value
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the min and max connection time from the settings, and calculate the range
|
||||
final int minPeerConnectionTime = Settings.getInstance().getMinPeerConnectionTime();
|
||||
final int maxPeerConnectionTime = Settings.getInstance().getMaxPeerConnectionTime();
|
||||
@@ -276,7 +295,7 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
protected void setLastPing(long lastPing) {
|
||||
public void setLastPing(long lastPing) {
|
||||
synchronized (this.peerInfoLock) {
|
||||
this.lastPing = lastPing;
|
||||
}
|
||||
@@ -346,12 +365,6 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
protected void queueMessage(Message message) {
|
||||
if (!this.pendingMessages.offer(message)) {
|
||||
LOGGER.info("[{}] No room to queue message from peer {} - discarding", this.peerConnectionId, this);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSyncInProgress() {
|
||||
return this.syncInProgress;
|
||||
}
|
||||
@@ -396,13 +409,14 @@ public class Peer {
|
||||
|
||||
// Processing
|
||||
|
||||
private void sharedSetup(Selector channelSelector) throws IOException {
|
||||
private void sharedSetup() throws IOException {
|
||||
this.connectionTimestamp = NTP.getTime();
|
||||
this.socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
|
||||
this.socketChannel.configureBlocking(false);
|
||||
this.socketChannel.register(channelSelector, SelectionKey.OP_READ);
|
||||
Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_READ);
|
||||
this.byteBuffer = null; // Defer allocation to when we need it, to save memory. Sorry GC!
|
||||
this.replyQueues = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
|
||||
this.sendQueue = new LinkedTransferQueue<>();
|
||||
this.replyQueues = new ConcurrentHashMap<>();
|
||||
this.pendingMessages = new LinkedBlockingQueue<>();
|
||||
|
||||
Random random = new SecureRandom();
|
||||
@@ -410,7 +424,7 @@ public class Peer {
|
||||
random.nextBytes(this.ourChallenge);
|
||||
}
|
||||
|
||||
public SocketChannel connect(Selector channelSelector) {
|
||||
public SocketChannel connect() {
|
||||
LOGGER.trace("[{}] Connecting to peer {}", this.peerConnectionId, this);
|
||||
|
||||
try {
|
||||
@@ -418,6 +432,8 @@ public class Peer {
|
||||
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
|
||||
|
||||
this.socketChannel = SocketChannel.open();
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
this.socketChannel.socket().bind(new InetSocketAddress(bindAddr, 0));
|
||||
this.socketChannel.socket().connect(resolvedAddress, CONNECT_TIMEOUT);
|
||||
} catch (SocketTimeoutException e) {
|
||||
LOGGER.trace("[{}] Connection timed out to peer {}", this.peerConnectionId, this);
|
||||
@@ -432,7 +448,7 @@ public class Peer {
|
||||
|
||||
try {
|
||||
LOGGER.debug("[{}] Connected to peer {}", this.peerConnectionId, this);
|
||||
sharedSetup(channelSelector);
|
||||
sharedSetup();
|
||||
return socketChannel;
|
||||
} catch (IOException e) {
|
||||
LOGGER.trace("[{}] Post-connection setup failed, peer {}", this.peerConnectionId, this);
|
||||
@@ -450,7 +466,7 @@ public class Peer {
|
||||
*
|
||||
* @throws IOException If this channel is not yet connected
|
||||
*/
|
||||
protected void readChannel() throws IOException {
|
||||
public void readChannel() throws IOException {
|
||||
synchronized (this.byteBufferLock) {
|
||||
while (true) {
|
||||
if (!this.socketChannel.isOpen() || this.socketChannel.socket().isClosed()) {
|
||||
@@ -531,6 +547,10 @@ public class Peer {
|
||||
// adjusting position accordingly, reset limit to capacity
|
||||
this.byteBuffer.compact();
|
||||
|
||||
// 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
|
||||
@@ -556,7 +576,67 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
protected ExecuteProduceConsume.Task getMessageTask() {
|
||||
/** Maybe send some pending outgoing messages.
|
||||
*
|
||||
* @return true if more data is pending to be sent
|
||||
*/
|
||||
public boolean writeChannel() throws IOException {
|
||||
// It is the responsibility of ChannelWriteTask's producer to produce only one call to writeChannel() at a time
|
||||
|
||||
while (true) {
|
||||
// If output byte buffer is null, fetch next message from queue (if any)
|
||||
while (this.outputBuffer == null) {
|
||||
Message message;
|
||||
|
||||
try {
|
||||
// Allow other thread time to add message to queue having raised OP_WRITE.
|
||||
// Timeout is overkill but not excessive enough to clog up networking / EPC.
|
||||
// This is to avoid race condition in sendMessageWithTimeout() below.
|
||||
message = this.sendQueue.poll(QUEUE_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// Shutdown situation
|
||||
return false;
|
||||
}
|
||||
|
||||
// No message? No further work to be done
|
||||
if (message == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
this.outputBuffer = ByteBuffer.wrap(message.toBytes());
|
||||
this.outputMessageType = message.getType().name();
|
||||
this.outputMessageId = message.getId();
|
||||
|
||||
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}",
|
||||
this.peerConnectionId, this.outputMessageType, this.outputMessageId, this);
|
||||
} 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,
|
||||
message.getType().name(), message.getId(), this, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// If output byte buffer is not null, send from that
|
||||
int bytesWritten = this.socketChannel.write(outputBuffer);
|
||||
|
||||
LOGGER.trace("[{}] Sent {} bytes of {} message with ID {} to peer {} ({} total)", this.peerConnectionId,
|
||||
bytesWritten, this.outputMessageType, this.outputMessageId, this, outputBuffer.limit());
|
||||
|
||||
// If we've sent 0 bytes then socket buffer is full so we need to wait until it's empty again
|
||||
if (bytesWritten == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we then exhaust the byte buffer, set it to null (otherwise loop and try to send more)
|
||||
if (!this.outputBuffer.hasRemaining()) {
|
||||
this.outputMessageType = null;
|
||||
this.outputMessageId = 0;
|
||||
this.outputBuffer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Task getMessageTask() {
|
||||
/*
|
||||
* If we are still handshaking and there is a message yet to be processed then
|
||||
* don't produce another message task. This allows us to process handshake
|
||||
@@ -580,7 +660,7 @@ public class Peer {
|
||||
}
|
||||
|
||||
// Return a task to process message in queue
|
||||
return () -> Network.getInstance().onMessage(this, nextMessage);
|
||||
return new MessageTask(this, nextMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -605,54 +685,25 @@ public class Peer {
|
||||
}
|
||||
|
||||
try {
|
||||
// Send message
|
||||
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}", this.peerConnectionId,
|
||||
// Queue message, to be picked up by ChannelWriteTask and then peer.writeChannel()
|
||||
LOGGER.trace("[{}] Queuing {} message with ID {} to peer {}", this.peerConnectionId,
|
||||
message.getType().name(), message.getId(), this);
|
||||
|
||||
ByteBuffer outputBuffer = ByteBuffer.wrap(message.toBytes());
|
||||
// Check message properly constructed
|
||||
message.checkValidOutgoing();
|
||||
|
||||
synchronized (this.socketChannel) {
|
||||
final long sendStart = System.currentTimeMillis();
|
||||
long totalBytes = 0;
|
||||
|
||||
while (outputBuffer.hasRemaining()) {
|
||||
int bytesWritten = this.socketChannel.write(outputBuffer);
|
||||
totalBytes += bytesWritten;
|
||||
|
||||
LOGGER.trace("[{}] Sent {} bytes of {} message with ID {} to peer {} ({} total)", this.peerConnectionId,
|
||||
bytesWritten, message.getType().name(), message.getId(), this, totalBytes);
|
||||
|
||||
if (bytesWritten == 0) {
|
||||
// Underlying socket's internal buffer probably full,
|
||||
// so wait a short while for bytes to actually be transmitted over the wire
|
||||
|
||||
/*
|
||||
* NOSONAR squid:S2276 - we don't want to use this.socketChannel.wait()
|
||||
* as this releases the lock held by synchronized() above
|
||||
* and would allow another thread to send another message,
|
||||
* potentially interleaving them on-the-wire, causing checksum failures
|
||||
* and connection loss.
|
||||
*/
|
||||
Thread.sleep(1L); //NOSONAR squid:S2276
|
||||
|
||||
if (System.currentTimeMillis() - sendStart > timeout) {
|
||||
// We've taken too long to send this message
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (MessageException e) {
|
||||
LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
|
||||
message.getType().name(), message.getId(), this, e.getMessage());
|
||||
return false;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// Possible race condition:
|
||||
// We set OP_WRITE, EPC creates ChannelWriteTask which calls Peer.writeChannel, writeChannel's poll() finds no message to send
|
||||
// Avoided by poll-with-timeout in writeChannel() above.
|
||||
Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_WRITE);
|
||||
return this.sendQueue.tryTransfer(message, timeout, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// Send failure
|
||||
return false;
|
||||
} catch (MessageException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sent OK
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -720,7 +771,7 @@ public class Peer {
|
||||
this.lastPingSent = NTP.getTime();
|
||||
}
|
||||
|
||||
protected ExecuteProduceConsume.Task getPingTask(Long now) {
|
||||
protected Task getPingTask(Long now) {
|
||||
// Pings not enabled yet?
|
||||
if (now == null || this.lastPingSent == null) {
|
||||
return null;
|
||||
@@ -734,19 +785,7 @@ public class Peer {
|
||||
// Not strictly true, but prevents this peer from being immediately chosen again
|
||||
this.lastPingSent = now;
|
||||
|
||||
return () -> {
|
||||
PingMessage pingMessage = new PingMessage();
|
||||
Message message = this.getResponse(pingMessage);
|
||||
|
||||
if (message == null || message.getType() != MessageType.PING) {
|
||||
LOGGER.debug("[{}] Didn't receive reply from {} for PING ID {}", this.peerConnectionId, this,
|
||||
pingMessage.getId());
|
||||
this.disconnect("no ping received");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLastPing(NTP.getTime() - now);
|
||||
};
|
||||
return new PingTask(this, now);
|
||||
}
|
||||
|
||||
public void disconnect(String reason) {
|
||||
@@ -877,6 +916,10 @@ public class Peer {
|
||||
return maxConnectionAge;
|
||||
}
|
||||
|
||||
public void setMaxConnectionAge(long maxConnectionAge) {
|
||||
this.maxConnectionAge = maxConnectionAge;
|
||||
}
|
||||
|
||||
public boolean hasReachedMaxConnectionAge() {
|
||||
return this.getConnectionAge() > this.getMaxConnectionAge();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class AccountBalanceMessage extends Message {
|
||||
|
||||
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||
|
||||
private AccountBalanceData accountBalanceData;
|
||||
|
||||
public AccountBalanceMessage(AccountBalanceData accountBalanceData) {
|
||||
super(MessageType.ACCOUNT_BALANCE);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
// Send raw address instead of base58 encoded
|
||||
byte[] address = Base58.decode(accountBalanceData.getAddress());
|
||||
bytes.write(address);
|
||||
|
||||
bytes.write(Longs.toByteArray(accountBalanceData.getAssetId()));
|
||||
|
||||
bytes.write(Longs.toByteArray(accountBalanceData.getBalance()));
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public AccountBalanceMessage(int id, AccountBalanceData accountBalanceData) {
|
||||
super(id, MessageType.ACCOUNT_BALANCE);
|
||||
|
||||
this.accountBalanceData = accountBalanceData;
|
||||
}
|
||||
|
||||
public AccountBalanceData getAccountBalanceData() {
|
||||
return this.accountBalanceData;
|
||||
}
|
||||
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||
byteBuffer.get(addressBytes);
|
||||
String address = Base58.encode(addressBytes);
|
||||
|
||||
long assetId = byteBuffer.getLong();
|
||||
|
||||
long balance = byteBuffer.getLong();
|
||||
|
||||
AccountBalanceData accountBalanceData = new AccountBalanceData(address, assetId, balance);
|
||||
return new AccountBalanceMessage(id, accountBalanceData);
|
||||
}
|
||||
|
||||
public AccountBalanceMessage cloneWithNewId(int newId) {
|
||||
AccountBalanceMessage clone = new AccountBalanceMessage(this.accountBalanceData);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
93
src/main/java/org/qortal/network/message/AccountMessage.java
Normal file
93
src/main/java/org/qortal/network/message/AccountMessage.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class AccountMessage extends Message {
|
||||
|
||||
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||
private static final int REFERENCE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
private static final int PUBLIC_KEY_LENGTH = Transformer.PUBLIC_KEY_LENGTH;
|
||||
|
||||
private AccountData accountData;
|
||||
|
||||
public AccountMessage(AccountData accountData) {
|
||||
super(MessageType.ACCOUNT);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
// Send raw address instead of base58 encoded
|
||||
byte[] address = Base58.decode(accountData.getAddress());
|
||||
bytes.write(address);
|
||||
|
||||
bytes.write(accountData.getReference());
|
||||
|
||||
bytes.write(accountData.getPublicKey());
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getDefaultGroupId()));
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getFlags()));
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getLevel()));
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getBlocksMinted()));
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment()));
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public AccountMessage(int id, AccountData accountData) {
|
||||
super(id, MessageType.ACCOUNT);
|
||||
|
||||
this.accountData = accountData;
|
||||
}
|
||||
|
||||
public AccountData getAccountData() {
|
||||
return this.accountData;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||
byteBuffer.get(addressBytes);
|
||||
String address = Base58.encode(addressBytes);
|
||||
|
||||
byte[] reference = new byte[REFERENCE_LENGTH];
|
||||
byteBuffer.get(reference);
|
||||
|
||||
byte[] publicKey = new byte[PUBLIC_KEY_LENGTH];
|
||||
byteBuffer.get(publicKey);
|
||||
|
||||
int defaultGroupId = byteBuffer.getInt();
|
||||
|
||||
int flags = byteBuffer.getInt();
|
||||
|
||||
int level = byteBuffer.getInt();
|
||||
|
||||
int blocksMinted = byteBuffer.getInt();
|
||||
|
||||
int blocksMintedAdjustment = byteBuffer.getInt();
|
||||
|
||||
AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
|
||||
return new AccountMessage(id, accountData);
|
||||
}
|
||||
|
||||
public AccountMessage cloneWithNewId(int newId) {
|
||||
AccountMessage clone = new AccountMessage(this.accountData);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,38 +9,59 @@ import org.qortal.utils.Serialization;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ArbitraryDataFileListMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
private static final int HASH_LENGTH = Transformer.SHA256_LENGTH;
|
||||
private static final int MAX_PEER_ADDRESS_LENGTH = PeerData.MAX_PEER_ADDRESS_SIZE;
|
||||
|
||||
private final byte[] signature;
|
||||
private final List<byte[]> hashes;
|
||||
private byte[] signature;
|
||||
private List<byte[]> hashes;
|
||||
private Long requestTime;
|
||||
private Integer requestHops;
|
||||
private String peerAddress;
|
||||
private Boolean isRelayPossible;
|
||||
|
||||
|
||||
public ArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes, Long requestTime,
|
||||
Integer requestHops, String peerAddress, boolean isRelayPossible) {
|
||||
Integer requestHops, String peerAddress, Boolean isRelayPossible) {
|
||||
super(MessageType.ARBITRARY_DATA_FILE_LIST);
|
||||
|
||||
this.signature = signature;
|
||||
this.hashes = hashes;
|
||||
this.requestTime = requestTime;
|
||||
this.requestHops = requestHops;
|
||||
this.peerAddress = peerAddress;
|
||||
this.isRelayPossible = isRelayPossible;
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(hashes.size()));
|
||||
|
||||
for (byte[] hash : hashes) {
|
||||
bytes.write(hash);
|
||||
}
|
||||
|
||||
if (requestTime != null) {
|
||||
// The remaining fields are optional
|
||||
|
||||
bytes.write(Longs.toByteArray(requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(requestHops));
|
||||
|
||||
Serialization.serializeSizedStringV2(bytes, peerAddress);
|
||||
|
||||
bytes.write(Ints.toByteArray(Boolean.TRUE.equals(isRelayPossible) ? 1 : 0));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public ArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, Long requestTime,
|
||||
/** Legacy version */
|
||||
public ArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes) {
|
||||
this(signature, hashes, null, null, null, null);
|
||||
}
|
||||
|
||||
private ArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, Long requestTime,
|
||||
Integer requestHops, String peerAddress, boolean isRelayPossible) {
|
||||
super(id, MessageType.ARBITRARY_DATA_FILE_LIST);
|
||||
|
||||
@@ -52,24 +73,39 @@ public class ArbitraryDataFileListMessage extends Message {
|
||||
this.isRelayPossible = isRelayPossible;
|
||||
}
|
||||
|
||||
public List<byte[]> getHashes() {
|
||||
return this.hashes;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public List<byte[]> getHashes() {
|
||||
return this.hashes;
|
||||
}
|
||||
|
||||
public Long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
public Integer getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public String getPeerAddress() {
|
||||
return this.peerAddress;
|
||||
}
|
||||
|
||||
public Boolean isRelayPossible() {
|
||||
return this.isRelayPossible;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
int count = bytes.getInt();
|
||||
|
||||
List<byte[]> hashes = new ArrayList<>();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
|
||||
byte[] hash = new byte[HASH_LENGTH];
|
||||
byte[] hash = new byte[Transformer.SHA256_LENGTH];
|
||||
bytes.get(hash);
|
||||
hashes.add(hash);
|
||||
}
|
||||
@@ -80,99 +116,21 @@ public class ArbitraryDataFileListMessage extends Message {
|
||||
boolean isRelayPossible = true; // Legacy versions only send this message when relaying is possible
|
||||
|
||||
// The remaining fields are optional
|
||||
|
||||
if (bytes.hasRemaining()) {
|
||||
try {
|
||||
requestTime = bytes.getLong();
|
||||
|
||||
requestTime = bytes.getLong();
|
||||
requestHops = bytes.getInt();
|
||||
|
||||
requestHops = bytes.getInt();
|
||||
|
||||
peerAddress = Serialization.deserializeSizedStringV2(bytes, MAX_PEER_ADDRESS_LENGTH);
|
||||
|
||||
isRelayPossible = bytes.getInt() > 0;
|
||||
peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
|
||||
|
||||
isRelayPossible = bytes.getInt() > 0;
|
||||
} catch (TransformationException e) {
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return new ArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops, peerAddress, isRelayPossible);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(this.hashes.size()));
|
||||
|
||||
for (byte[] hash : this.hashes) {
|
||||
bytes.write(hash);
|
||||
}
|
||||
|
||||
if (this.requestTime == null) { // To maintain backwards support
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
// The remaining fields are optional
|
||||
|
||||
bytes.write(Longs.toByteArray(this.requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(this.requestHops));
|
||||
|
||||
Serialization.serializeSizedStringV2(bytes, this.peerAddress);
|
||||
|
||||
bytes.write(Ints.toByteArray(this.isRelayPossible ? 1 : 0));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ArbitraryDataFileListMessage cloneWithNewId(int newId) {
|
||||
ArbitraryDataFileListMessage clone = new ArbitraryDataFileListMessage(this.signature, this.hashes,
|
||||
this.requestTime, this.requestHops, this.peerAddress, this.isRelayPossible);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public void removeOptionalStats() {
|
||||
this.requestTime = null;
|
||||
this.requestHops = null;
|
||||
this.peerAddress = null;
|
||||
this.isRelayPossible = null;
|
||||
}
|
||||
|
||||
public Long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
public void setRequestTime(Long requestTime) {
|
||||
this.requestTime = requestTime;
|
||||
}
|
||||
|
||||
public Integer getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public void setRequestHops(Integer requestHops) {
|
||||
this.requestHops = requestHops;
|
||||
}
|
||||
|
||||
public String getPeerAddress() {
|
||||
return this.peerAddress;
|
||||
}
|
||||
|
||||
public void setPeerAddress(String peerAddress) {
|
||||
this.peerAddress = peerAddress;
|
||||
}
|
||||
|
||||
public Boolean isRelayPossible() {
|
||||
return this.isRelayPossible;
|
||||
}
|
||||
|
||||
public void setIsRelayPossible(Boolean isRelayPossible) {
|
||||
this.isRelayPossible = isRelayPossible;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,44 +9,60 @@ import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ArbitraryDataFileMessage extends Message {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileMessage.class);
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private final byte[] signature;
|
||||
private final ArbitraryDataFile arbitraryDataFile;
|
||||
private byte[] signature;
|
||||
private ArbitraryDataFile arbitraryDataFile;
|
||||
|
||||
public ArbitraryDataFileMessage(byte[] signature, ArbitraryDataFile arbitraryDataFile) {
|
||||
super(MessageType.ARBITRARY_DATA_FILE);
|
||||
|
||||
this.signature = signature;
|
||||
this.arbitraryDataFile = arbitraryDataFile;
|
||||
byte[] data = arbitraryDataFile.getBytes();
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public ArbitraryDataFileMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
|
||||
private ArbitraryDataFileMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
|
||||
super(id, MessageType.ARBITRARY_DATA_FILE);
|
||||
|
||||
this.signature = signature;
|
||||
this.arbitraryDataFile = arbitraryDataFile;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public ArbitraryDataFile getArbitraryDataFile() {
|
||||
return this.arbitraryDataFile;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
int dataLength = byteBuffer.getInt();
|
||||
|
||||
if (byteBuffer.remaining() != dataLength)
|
||||
return null;
|
||||
if (byteBuffer.remaining() < dataLength)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
byte[] data = new byte[dataLength];
|
||||
byteBuffer.get(data);
|
||||
@@ -54,43 +70,10 @@ public class ArbitraryDataFileMessage extends Message {
|
||||
try {
|
||||
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(data, signature);
|
||||
return new ArbitraryDataFileMessage(id, signature, arbitraryDataFile);
|
||||
}
|
||||
catch (DataException e) {
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to process received file: {}", e.getMessage());
|
||||
return null;
|
||||
throw new MessageException("Unable to process received file: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
if (this.arbitraryDataFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] data = this.arbitraryDataFile.getBytes();
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ArbitraryDataFileMessage cloneWithNewId(int newId) {
|
||||
ArbitraryDataFileMessage clone = new ArbitraryDataFileMessage(this.signature, this.arbitraryDataFile);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
@@ -11,13 +11,26 @@ import com.google.common.primitives.Ints;
|
||||
|
||||
public class ArbitraryDataMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private byte[] signature;
|
||||
private byte[] data;
|
||||
|
||||
public ArbitraryDataMessage(byte[] signature, byte[] data) {
|
||||
this(-1, signature, data);
|
||||
super(MessageType.ARBITRARY_DATA);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private ArbitraryDataMessage(int id, byte[] signature, byte[] data) {
|
||||
@@ -35,14 +48,14 @@ public class ArbitraryDataMessage extends Message {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
int dataLength = byteBuffer.getInt();
|
||||
|
||||
if (byteBuffer.remaining() != dataLength)
|
||||
return null;
|
||||
if (byteBuffer.remaining() < dataLength)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
byte[] data = new byte[dataLength];
|
||||
byteBuffer.get(data);
|
||||
@@ -50,24 +63,4 @@ public class ArbitraryDataMessage extends Message {
|
||||
return new ArbitraryDataMessage(id, signature, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
if (this.data == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(this.data.length));
|
||||
|
||||
bytes.write(this.data);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,28 +7,40 @@ import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ArbitraryMetadataMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
private byte[] signature;
|
||||
private ArbitraryDataFile arbitraryMetadataFile;
|
||||
|
||||
private final byte[] signature;
|
||||
private final ArbitraryDataFile arbitraryMetadataFile;
|
||||
|
||||
public ArbitraryMetadataMessage(byte[] signature, ArbitraryDataFile arbitraryDataFile) {
|
||||
public ArbitraryMetadataMessage(byte[] signature, ArbitraryDataFile arbitraryMetadataFile) {
|
||||
super(MessageType.ARBITRARY_METADATA);
|
||||
|
||||
this.signature = signature;
|
||||
this.arbitraryMetadataFile = arbitraryDataFile;
|
||||
byte[] data = arbitraryMetadataFile.getBytes();
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public ArbitraryMetadataMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
|
||||
private ArbitraryMetadataMessage(int id, byte[] signature, ArbitraryDataFile arbitraryMetadataFile) {
|
||||
super(id, MessageType.ARBITRARY_METADATA);
|
||||
|
||||
this.signature = signature;
|
||||
this.arbitraryMetadataFile = arbitraryDataFile;
|
||||
this.arbitraryMetadataFile = arbitraryMetadataFile;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
@@ -39,14 +51,14 @@ public class ArbitraryMetadataMessage extends Message {
|
||||
return this.arbitraryMetadataFile;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
int dataLength = byteBuffer.getInt();
|
||||
|
||||
if (byteBuffer.remaining() != dataLength)
|
||||
return null;
|
||||
if (byteBuffer.remaining() < dataLength)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
byte[] data = new byte[dataLength];
|
||||
byteBuffer.get(data);
|
||||
@@ -54,42 +66,9 @@ public class ArbitraryMetadataMessage extends Message {
|
||||
try {
|
||||
ArbitraryDataFile arbitraryMetadataFile = new ArbitraryDataFile(data, signature);
|
||||
return new ArbitraryMetadataMessage(id, signature, arbitraryMetadataFile);
|
||||
} catch (DataException e) {
|
||||
throw new MessageException("Unable to process arbitrary metadata message: " + e.getMessage(), e);
|
||||
}
|
||||
catch (DataException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
if (this.arbitraryMetadataFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] data = this.arbitraryMetadataFile.getBytes();
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ArbitraryMetadataMessage cloneWithNewId(int newId) {
|
||||
ArbitraryMetadataMessage clone = new ArbitraryMetadataMessage(this.signature, this.arbitraryMetadataFile);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,21 +8,37 @@ import org.qortal.utils.Serialization;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ArbitrarySignaturesMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private String peerAddress;
|
||||
private int requestHops;
|
||||
private List<byte[]> signatures;
|
||||
|
||||
public ArbitrarySignaturesMessage(String peerAddress, int requestHops, List<byte[]> signatures) {
|
||||
this(-1, peerAddress, requestHops, signatures);
|
||||
super(MessageType.ARBITRARY_SIGNATURES);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
Serialization.serializeSizedStringV2(bytes, peerAddress);
|
||||
|
||||
bytes.write(Ints.toByteArray(requestHops));
|
||||
|
||||
bytes.write(Ints.toByteArray(signatures.size()));
|
||||
|
||||
for (byte[] signature : signatures)
|
||||
bytes.write(signature);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private ArbitrarySignaturesMessage(int id, String peerAddress, int requestHops, List<byte[]> signatures) {
|
||||
@@ -41,27 +57,24 @@ public class ArbitrarySignaturesMessage extends Message {
|
||||
return this.signatures;
|
||||
}
|
||||
|
||||
public int getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public void setRequestHops(int requestHops) {
|
||||
this.requestHops = requestHops;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
|
||||
String peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
String peerAddress;
|
||||
try {
|
||||
peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
|
||||
} catch (TransformationException e) {
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
int requestHops = bytes.getInt();
|
||||
|
||||
int signatureCount = bytes.getInt();
|
||||
|
||||
if (bytes.remaining() != signatureCount * SIGNATURE_LENGTH)
|
||||
return null;
|
||||
if (bytes.remaining() < signatureCount * Transformer.SIGNATURE_LENGTH)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
for (int i = 0; i < signatureCount; ++i) {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
signatures.add(signature);
|
||||
}
|
||||
@@ -69,24 +82,4 @@ public class ArbitrarySignaturesMessage extends Message {
|
||||
return new ArbitrarySignaturesMessage(id, peerAddress, requestHops, signatures);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
Serialization.serializeSizedStringV2(bytes, this.peerAddress);
|
||||
|
||||
bytes.write(Ints.toByteArray(this.requestHops));
|
||||
|
||||
bytes.write(Ints.toByteArray(this.signatures.size()));
|
||||
|
||||
for (byte[] signature : this.signatures)
|
||||
bytes.write(signature);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,42 +1,27 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.Block;
|
||||
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;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
public class BlockMessage extends Message {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockMessage.class);
|
||||
|
||||
private Block block = null;
|
||||
private final BlockData blockData;
|
||||
private final List<TransactionData> transactions;
|
||||
private final List<ATStateData> atStates;
|
||||
|
||||
private BlockData blockData = null;
|
||||
private List<TransactionData> transactions = null;
|
||||
private List<ATStateData> atStates = null;
|
||||
|
||||
private int height;
|
||||
|
||||
public BlockMessage(Block block) {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = block;
|
||||
this.blockData = block.getBlockData();
|
||||
this.height = block.getBlockData().getHeight();
|
||||
}
|
||||
// No public constructor as we're an incoming-only message type.
|
||||
|
||||
private BlockMessage(int id, BlockData blockData, List<TransactionData> transactions, List<ATStateData> atStates) {
|
||||
super(id, MessageType.BLOCK);
|
||||
@@ -44,8 +29,6 @@ public class BlockMessage extends Message {
|
||||
this.blockData = blockData;
|
||||
this.transactions = transactions;
|
||||
this.atStates = atStates;
|
||||
|
||||
this.height = blockData.getHeight();
|
||||
}
|
||||
|
||||
public BlockData getBlockData() {
|
||||
@@ -60,44 +43,20 @@ public class BlockMessage extends Message {
|
||||
return this.atStates;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
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()));
|
||||
return null;
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
if (this.block == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.height));
|
||||
|
||||
bytes.write(BlockTransformer.toBytes(this.block));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (TransformationException | IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BlockMessage cloneWithNewId(int newId) {
|
||||
BlockMessage clone = new BlockMessage(this.block);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -20,7 +20,25 @@ public class BlockSummariesMessage extends Message {
|
||||
private List<BlockSummaryData> blockSummaries;
|
||||
|
||||
public BlockSummariesMessage(List<BlockSummaryData> blockSummaries) {
|
||||
this(-1, blockSummaries);
|
||||
super(MessageType.BLOCK_SUMMARIES);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(blockSummaries.size()));
|
||||
|
||||
for (BlockSummaryData blockSummary : blockSummaries) {
|
||||
bytes.write(Ints.toByteArray(blockSummary.getHeight()));
|
||||
bytes.write(blockSummary.getSignature());
|
||||
bytes.write(blockSummary.getMinterPublicKey());
|
||||
bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private BlockSummariesMessage(int id, List<BlockSummaryData> blockSummaries) {
|
||||
@@ -33,11 +51,11 @@ public class BlockSummariesMessage extends Message {
|
||||
return this.blockSummaries;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
int count = bytes.getInt();
|
||||
|
||||
if (bytes.remaining() != count * BLOCK_SUMMARY_LENGTH)
|
||||
return null;
|
||||
if (bytes.remaining() < count * BLOCK_SUMMARY_LENGTH)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
@@ -58,24 +76,4 @@ public class BlockSummariesMessage extends Message {
|
||||
return new BlockSummariesMessage(id, blockSummaries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.blockSummaries.size()));
|
||||
|
||||
for (BlockSummaryData blockSummary : this.blockSummaries) {
|
||||
bytes.write(Ints.toByteArray(blockSummary.getHeight()));
|
||||
bytes.write(blockSummary.getSignature());
|
||||
bytes.write(blockSummary.getMinterPublicKey());
|
||||
bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.qortal.block.Block;
|
||||
@@ -12,59 +11,34 @@ import org.qortal.transform.block.BlockTransformer;
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
// This is an OUTGOING-only Message which more readily lends itself to being cached
|
||||
public class CachedBlockMessage extends Message {
|
||||
public class CachedBlockMessage extends Message implements Cloneable {
|
||||
|
||||
private Block block = null;
|
||||
private byte[] cachedBytes = null;
|
||||
|
||||
public CachedBlockMessage(Block block) {
|
||||
public CachedBlockMessage(Block block) throws TransformationException {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = block;
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public CachedBlockMessage(byte[] cachedBytes) {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = null;
|
||||
this.cachedBytes = cachedBytes;
|
||||
this.dataBytes = cachedBytes;
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
throw new UnsupportedOperationException("CachedBlockMessage is for outgoing messages only");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
// Already serialized?
|
||||
if (this.cachedBytes != null)
|
||||
return cachedBytes;
|
||||
|
||||
if (this.block == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.block.getBlockData().getHeight()));
|
||||
|
||||
bytes.write(BlockTransformer.toBytes(this.block));
|
||||
|
||||
this.cachedBytes = bytes.toByteArray();
|
||||
// We no longer need source Block
|
||||
// and Block contains repository handle which is highly likely to be invalid after this call
|
||||
this.block = null;
|
||||
|
||||
return this.cachedBytes;
|
||||
} catch (TransformationException | IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public CachedBlockMessage cloneWithNewId(int newId) {
|
||||
CachedBlockMessage clone = new CachedBlockMessage(this.cachedBytes);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,8 +10,25 @@ public class ChallengeMessage extends Message {
|
||||
|
||||
public static final int CHALLENGE_LENGTH = 32;
|
||||
|
||||
private final byte[] publicKey;
|
||||
private final byte[] challenge;
|
||||
private byte[] publicKey;
|
||||
private byte[] challenge;
|
||||
|
||||
public ChallengeMessage(byte[] publicKey, byte[] challenge) {
|
||||
super(MessageType.CHALLENGE);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(publicKey.length + challenge.length);
|
||||
|
||||
try {
|
||||
bytes.write(publicKey);
|
||||
|
||||
bytes.write(challenge);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private ChallengeMessage(int id, byte[] publicKey, byte[] challenge) {
|
||||
super(id, MessageType.CHALLENGE);
|
||||
@@ -20,10 +37,6 @@ public class ChallengeMessage extends Message {
|
||||
this.challenge = challenge;
|
||||
}
|
||||
|
||||
public ChallengeMessage(byte[] publicKey, byte[] challenge) {
|
||||
this(-1, publicKey, challenge);
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
@@ -42,15 +55,4 @@ public class ChallengeMessage extends Message {
|
||||
return new ChallengeMessage(id, publicKey, challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.publicKey);
|
||||
|
||||
bytes.write(this.challenge);
|
||||
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetAccountBalanceMessage extends Message {
|
||||
|
||||
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||
|
||||
private String address;
|
||||
private long assetId;
|
||||
|
||||
public GetAccountBalanceMessage(String address, long assetId) {
|
||||
super(MessageType.GET_ACCOUNT_BALANCE);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
// Send raw address instead of base58 encoded
|
||||
byte[] addressBytes = Base58.decode(address);
|
||||
bytes.write(addressBytes);
|
||||
|
||||
bytes.write(Longs.toByteArray(assetId));
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetAccountBalanceMessage(int id, String address, long assetId) {
|
||||
super(id, MessageType.GET_ACCOUNT_BALANCE);
|
||||
|
||||
this.address = address;
|
||||
this.assetId = assetId;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public long getAssetId() {
|
||||
return this.assetId;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||
bytes.get(addressBytes);
|
||||
String address = Base58.encode(addressBytes);
|
||||
|
||||
long assetId = bytes.getLong();
|
||||
|
||||
return new GetAccountBalanceMessage(id, address, assetId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetAccountMessage extends Message {
|
||||
|
||||
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||
|
||||
private String address;
|
||||
|
||||
public GetAccountMessage(String address) {
|
||||
super(MessageType.GET_ACCOUNT);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
// Send raw address instead of base58 encoded
|
||||
byte[] addressBytes = Base58.decode(address);
|
||||
bytes.write(addressBytes);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetAccountMessage(int id, String address) {
|
||||
super(id, MessageType.GET_ACCOUNT);
|
||||
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
if (bytes.remaining() != ADDRESS_LENGTH)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||
bytes.get(addressBytes);
|
||||
String address = Base58.encode(addressBytes);
|
||||
|
||||
return new GetAccountMessage(id, address);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetAccountNamesMessage extends Message {
|
||||
|
||||
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||
|
||||
private String address;
|
||||
|
||||
public GetAccountNamesMessage(String address) {
|
||||
super(MessageType.GET_ACCOUNT_NAMES);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
// Send raw address instead of base58 encoded
|
||||
byte[] addressBytes = Base58.decode(address);
|
||||
bytes.write(addressBytes);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetAccountNamesMessage(int id, String address) {
|
||||
super(id, MessageType.GET_ACCOUNT_NAMES);
|
||||
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||
bytes.get(addressBytes);
|
||||
String address = Base58.encode(addressBytes);
|
||||
|
||||
return new GetAccountNamesMessage(id, address);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetAccountTransactionsMessage extends Message {
|
||||
|
||||
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||
|
||||
private String address;
|
||||
private int limit;
|
||||
private int offset;
|
||||
|
||||
public GetAccountTransactionsMessage(String address, int limit, int offset) {
|
||||
super(MessageType.GET_ACCOUNT_TRANSACTIONS);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
// Send raw address instead of base58 encoded
|
||||
byte[] addressBytes = Base58.decode(address);
|
||||
bytes.write(addressBytes);
|
||||
|
||||
bytes.write(Ints.toByteArray(limit));
|
||||
|
||||
bytes.write(Ints.toByteArray(offset));
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetAccountTransactionsMessage(int id, String address, int limit, int offset) {
|
||||
super(id, MessageType.GET_ACCOUNT_TRANSACTIONS);
|
||||
|
||||
this.address = address;
|
||||
this.limit = limit;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public int getLimit() { return this.limit; }
|
||||
|
||||
public int getOffset() { return this.offset; }
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||
bytes.get(addressBytes);
|
||||
String address = Base58.encode(addressBytes);
|
||||
|
||||
int limit = bytes.getInt();
|
||||
|
||||
int offset = bytes.getInt();
|
||||
|
||||
return new GetAccountTransactionsMessage(id, address, limit, offset);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,33 +5,54 @@ import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Serialization;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.qortal.transform.Transformer.INT_LENGTH;
|
||||
import static org.qortal.transform.Transformer.LONG_LENGTH;
|
||||
|
||||
public class GetArbitraryDataFileListMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
private static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
||||
private static final int MAX_PEER_ADDRESS_LENGTH = PeerData.MAX_PEER_ADDRESS_SIZE;
|
||||
|
||||
private final byte[] signature;
|
||||
private byte[] signature;
|
||||
private List<byte[]> hashes;
|
||||
private final long requestTime;
|
||||
private long requestTime;
|
||||
private int requestHops;
|
||||
private String requestingPeer;
|
||||
|
||||
public GetArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes, long requestTime, int requestHops, String requestingPeer) {
|
||||
this(-1, signature, hashes, requestTime, requestHops, requestingPeer);
|
||||
super(MessageType.GET_ARBITRARY_DATA_FILE_LIST);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(requestHops));
|
||||
|
||||
if (hashes != null) {
|
||||
bytes.write(Ints.toByteArray(hashes.size()));
|
||||
|
||||
for (byte[] hash : hashes) {
|
||||
bytes.write(hash);
|
||||
}
|
||||
}
|
||||
else {
|
||||
bytes.write(Ints.toByteArray(0));
|
||||
}
|
||||
|
||||
if (requestingPeer != null) {
|
||||
Serialization.serializeSizedStringV2(bytes, requestingPeer);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, long requestTime, int requestHops, String requestingPeer) {
|
||||
@@ -52,8 +73,20 @@ public class GetArbitraryDataFileListMessage extends Message {
|
||||
return this.hashes;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
public int getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public String getRequestingPeer() {
|
||||
return this.requestingPeer;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
|
||||
bytes.get(signature);
|
||||
|
||||
@@ -67,7 +100,7 @@ public class GetArbitraryDataFileListMessage extends Message {
|
||||
|
||||
hashes = new ArrayList<>();
|
||||
for (int i = 0; i < hashCount; ++i) {
|
||||
byte[] hash = new byte[HASH_LENGTH];
|
||||
byte[] hash = new byte[Transformer.SHA256_LENGTH];
|
||||
bytes.get(hash);
|
||||
hashes.add(hash);
|
||||
}
|
||||
@@ -75,57 +108,14 @@ public class GetArbitraryDataFileListMessage extends Message {
|
||||
|
||||
String requestingPeer = null;
|
||||
if (bytes.hasRemaining()) {
|
||||
requestingPeer = Serialization.deserializeSizedStringV2(bytes, MAX_PEER_ADDRESS_LENGTH);
|
||||
try {
|
||||
requestingPeer = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
|
||||
} catch (TransformationException e) {
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return new GetArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops, requestingPeer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(this.requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(this.requestHops));
|
||||
|
||||
if (this.hashes != null) {
|
||||
bytes.write(Ints.toByteArray(this.hashes.size()));
|
||||
|
||||
for (byte[] hash : this.hashes) {
|
||||
bytes.write(hash);
|
||||
}
|
||||
}
|
||||
else {
|
||||
bytes.write(Ints.toByteArray(0));
|
||||
}
|
||||
|
||||
if (this.requestingPeer != null) {
|
||||
Serialization.serializeSizedStringV2(bytes, this.requestingPeer);
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
public int getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
public void setRequestHops(int requestHops) {
|
||||
this.requestHops = requestHops;
|
||||
}
|
||||
|
||||
public String getRequestingPeer() {
|
||||
return this.requestingPeer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetArbitraryDataFileMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
private static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
||||
|
||||
private final byte[] signature;
|
||||
private final byte[] hash;
|
||||
private byte[] signature;
|
||||
private byte[] hash;
|
||||
|
||||
public GetArbitraryDataFileMessage(byte[] signature, byte[] hash) {
|
||||
this(-1, signature, hash);
|
||||
super(MessageType.GET_ARBITRARY_DATA_FILE);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(signature.length + hash.length);
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(hash);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetArbitraryDataFileMessage(int id, byte[] signature, byte[] hash) {
|
||||
@@ -35,32 +43,14 @@ public class GetArbitraryDataFileMessage extends Message {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != HASH_LENGTH + SIGNATURE_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
byte[] hash = new byte[HASH_LENGTH];
|
||||
byte[] hash = new byte[Transformer.SHA256_LENGTH];
|
||||
bytes.get(hash);
|
||||
|
||||
return new GetArbitraryDataFileMessage(id, signature, hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(this.hash);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
public class GetArbitraryDataMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private byte[] signature;
|
||||
|
||||
public GetArbitraryDataMessage(byte[] signature) {
|
||||
this(-1, signature);
|
||||
super(MessageType.GET_ARBITRARY_DATA);
|
||||
|
||||
this.dataBytes = Arrays.copyOf(signature, signature.length);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetArbitraryDataMessage(int id, byte[] signature) {
|
||||
@@ -27,28 +26,12 @@ public class GetArbitraryDataMessage extends Message {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != SIGNATURE_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
|
||||
bytes.get(signature);
|
||||
|
||||
return new GetArbitraryDataMessage(id, signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,22 +6,31 @@ import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.qortal.transform.Transformer.INT_LENGTH;
|
||||
import static org.qortal.transform.Transformer.LONG_LENGTH;
|
||||
|
||||
public class GetArbitraryMetadataMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private final byte[] signature;
|
||||
private final long requestTime;
|
||||
private byte[] signature;
|
||||
private long requestTime;
|
||||
private int requestHops;
|
||||
|
||||
public GetArbitraryMetadataMessage(byte[] signature, long requestTime, int requestHops) {
|
||||
this(-1, signature, requestTime, requestHops);
|
||||
super(MessageType.GET_ARBITRARY_METADATA);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(requestHops));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetArbitraryMetadataMessage(int id, byte[] signature, long requestTime, int requestHops) {
|
||||
@@ -36,12 +45,16 @@ public class GetArbitraryMetadataMessage extends Message {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != SIGNATURE_LENGTH + LONG_LENGTH + INT_LENGTH)
|
||||
return null;
|
||||
public long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public int getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
long requestTime = bytes.getLong();
|
||||
@@ -51,33 +64,4 @@ public class GetArbitraryMetadataMessage extends Message {
|
||||
return new GetArbitraryMetadataMessage(id, signature, requestTime, requestHops);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(this.requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(this.requestHops));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
public int getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public void setRequestHops(int requestHops) {
|
||||
this.requestHops = requestHops;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
public class GetBlockMessage extends Message {
|
||||
|
||||
private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
|
||||
|
||||
private byte[] signature;
|
||||
|
||||
public GetBlockMessage(byte[] signature) {
|
||||
this(-1, signature);
|
||||
super(MessageType.GET_BLOCK);
|
||||
|
||||
this.dataBytes = Arrays.copyOf(signature, signature.length);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetBlockMessage(int id, byte[] signature) {
|
||||
@@ -27,28 +26,11 @@ public class GetBlockMessage extends Message {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] signature = new byte[BLOCK_SIGNATURE_LENGTH];
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
return new GetBlockMessage(id, signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,23 +2,32 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
public class GetBlockSummariesMessage extends Message {
|
||||
|
||||
private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
|
||||
|
||||
private byte[] parentSignature;
|
||||
private int numberRequested;
|
||||
|
||||
public GetBlockSummariesMessage(byte[] parentSignature, int numberRequested) {
|
||||
this(-1, parentSignature, numberRequested);
|
||||
super(MessageType.GET_BLOCK_SUMMARIES);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(parentSignature);
|
||||
|
||||
bytes.write(Ints.toByteArray(numberRequested));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetBlockSummariesMessage(int id, byte[] parentSignature, int numberRequested) {
|
||||
@@ -36,11 +45,8 @@ public class GetBlockSummariesMessage extends Message {
|
||||
return this.numberRequested;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH + Transformer.INT_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] parentSignature = new byte[BLOCK_SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] parentSignature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
|
||||
bytes.get(parentSignature);
|
||||
|
||||
int numberRequested = bytes.getInt();
|
||||
@@ -48,19 +54,4 @@ public class GetBlockSummariesMessage extends Message {
|
||||
return new GetBlockSummariesMessage(id, parentSignature, numberRequested);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.parentSignature);
|
||||
|
||||
bytes.write(Ints.toByteArray(this.numberRequested));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
53
src/main/java/org/qortal/network/message/GetNameMessage.java
Normal file
53
src/main/java/org/qortal/network/message/GetNameMessage.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import org.qortal.naming.Name;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.utils.Serialization;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetNameMessage extends Message {
|
||||
|
||||
private String name;
|
||||
|
||||
public GetNameMessage(String address) {
|
||||
super(MessageType.GET_NAME);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
Serialization.serializeSizedStringV2(bytes, name);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetNameMessage(int id, String name) {
|
||||
super(id, MessageType.GET_NAME);
|
||||
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
try {
|
||||
String name = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE);
|
||||
|
||||
return new GetNameMessage(id, name);
|
||||
|
||||
} catch (TransformationException e) {
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -20,7 +19,24 @@ public class GetOnlineAccountsMessage extends Message {
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
|
||||
public GetOnlineAccountsMessage(List<OnlineAccountData> onlineAccounts) {
|
||||
this(-1, onlineAccounts);
|
||||
super(MessageType.GET_ONLINE_ACCOUNTS);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(onlineAccounts.size()));
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
|
||||
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetOnlineAccountsMessage(int id, List<OnlineAccountData> onlineAccounts) {
|
||||
@@ -33,7 +49,7 @@ public class GetOnlineAccountsMessage extends Message {
|
||||
return this.onlineAccounts;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
final int accountCount = bytes.getInt();
|
||||
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
|
||||
@@ -50,24 +66,4 @@ public class GetOnlineAccountsMessage extends Message {
|
||||
return new GetOnlineAccountsMessage(id, onlineAccounts);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.onlineAccounts.size()));
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
|
||||
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -24,11 +23,51 @@ import java.util.Map;
|
||||
* Also V2 only builds online accounts message once!
|
||||
*/
|
||||
public class GetOnlineAccountsV2Message extends Message {
|
||||
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
private byte[] cachedData;
|
||||
|
||||
public GetOnlineAccountsV2Message(List<OnlineAccountData> onlineAccounts) {
|
||||
this(-1, onlineAccounts);
|
||||
super(MessageType.GET_ONLINE_ACCOUNTS_V2);
|
||||
|
||||
// If we don't have ANY online accounts then it's an easier construction...
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
// Always supply a number of accounts
|
||||
this.dataBytes = Ints.toByteArray(0);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
// How many of each timestamp
|
||||
Map<Long, Integer> countByTimestamp = new HashMap<>();
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
Long timestamp = onlineAccountData.getTimestamp();
|
||||
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
|
||||
+ onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH;
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
try {
|
||||
for (long timestamp : countByTimestamp.keySet()) {
|
||||
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
if (onlineAccountData.getTimestamp() == timestamp)
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetOnlineAccountsV2Message(int id, List<OnlineAccountData> onlineAccounts) {
|
||||
@@ -41,7 +80,7 @@ public class GetOnlineAccountsV2Message extends Message {
|
||||
return this.onlineAccounts;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
int accountCount = bytes.getInt();
|
||||
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
|
||||
@@ -67,51 +106,4 @@ public class GetOnlineAccountsV2Message extends Message {
|
||||
return new GetOnlineAccountsV2Message(id, onlineAccounts);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized byte[] toData() {
|
||||
if (this.cachedData != null)
|
||||
return this.cachedData;
|
||||
|
||||
// Shortcut in case we have no online accounts
|
||||
if (this.onlineAccounts.isEmpty()) {
|
||||
this.cachedData = Ints.toByteArray(0);
|
||||
return this.cachedData;
|
||||
}
|
||||
|
||||
// How many of each timestamp
|
||||
Map<Long, Integer> countByTimestamp = new HashMap<>();
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
Long timestamp = onlineAccountData.getTimestamp();
|
||||
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
|
||||
+ this.onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
for (long timestamp : countByTimestamp.keySet()) {
|
||||
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
|
||||
if (onlineAccountData.getTimestamp() == timestamp)
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedData = bytes.toByteArray();
|
||||
return this.cachedData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetPeersMessage extends Message {
|
||||
|
||||
public GetPeersMessage() {
|
||||
this(-1);
|
||||
super(MessageType.GET_PEERS);
|
||||
|
||||
this.dataBytes = EMPTY_DATA_BYTES;
|
||||
}
|
||||
|
||||
private GetPeersMessage(int id) {
|
||||
super(id, MessageType.GET_PEERS);
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
return new GetPeersMessage(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,24 +2,32 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
public class GetSignaturesV2Message extends Message {
|
||||
|
||||
private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
|
||||
private static final int NUMBER_REQUESTED_LENGTH = Transformer.INT_LENGTH;
|
||||
|
||||
private byte[] parentSignature;
|
||||
private int numberRequested;
|
||||
|
||||
public GetSignaturesV2Message(byte[] parentSignature, int numberRequested) {
|
||||
this(-1, parentSignature, numberRequested);
|
||||
super(MessageType.GET_SIGNATURES_V2);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(parentSignature);
|
||||
|
||||
bytes.write(Ints.toByteArray(numberRequested));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetSignaturesV2Message(int id, byte[] parentSignature, int numberRequested) {
|
||||
@@ -37,11 +45,8 @@ public class GetSignaturesV2Message extends Message {
|
||||
return this.numberRequested;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH + NUMBER_REQUESTED_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] parentSignature = new byte[BLOCK_SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] parentSignature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
|
||||
bytes.get(parentSignature);
|
||||
|
||||
int numberRequested = bytes.getInt();
|
||||
@@ -49,19 +54,4 @@ public class GetSignaturesV2Message extends Message {
|
||||
return new GetSignaturesV2Message(id, parentSignature, numberRequested);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.parentSignature);
|
||||
|
||||
bytes.write(Ints.toByteArray(this.numberRequested));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -21,10 +20,48 @@ import java.util.Map;
|
||||
*/
|
||||
public class GetTradePresencesMessage extends Message {
|
||||
private List<TradePresenceData> tradePresences;
|
||||
private byte[] cachedData;
|
||||
|
||||
public GetTradePresencesMessage(List<TradePresenceData> tradePresences) {
|
||||
this(-1, tradePresences);
|
||||
super(MessageType.GET_TRADE_PRESENCES);
|
||||
|
||||
// Shortcut in case we have no trade presences
|
||||
if (tradePresences.isEmpty()) {
|
||||
this.dataBytes = Ints.toByteArray(0);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
// How many of each timestamp
|
||||
Map<Long, Integer> countByTimestamp = new HashMap<>();
|
||||
|
||||
for (TradePresenceData tradePresenceData : tradePresences) {
|
||||
Long timestamp = tradePresenceData.getTimestamp();
|
||||
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
|
||||
+ tradePresences.size() * Transformer.PUBLIC_KEY_LENGTH;
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
try {
|
||||
for (long timestamp : countByTimestamp.keySet()) {
|
||||
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
for (TradePresenceData tradePresenceData : tradePresences) {
|
||||
if (tradePresenceData.getTimestamp() == timestamp)
|
||||
bytes.write(tradePresenceData.getPublicKey());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetTradePresencesMessage(int id, List<TradePresenceData> tradePresences) {
|
||||
@@ -37,7 +74,7 @@ public class GetTradePresencesMessage extends Message {
|
||||
return this.tradePresences;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
int groupedEntriesCount = bytes.getInt();
|
||||
|
||||
List<TradePresenceData> tradePresences = new ArrayList<>(groupedEntriesCount);
|
||||
@@ -63,48 +100,4 @@ public class GetTradePresencesMessage extends Message {
|
||||
return new GetTradePresencesMessage(id, tradePresences);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized byte[] toData() {
|
||||
if (this.cachedData != null)
|
||||
return this.cachedData;
|
||||
|
||||
// Shortcut in case we have no trade presences
|
||||
if (this.tradePresences.isEmpty()) {
|
||||
this.cachedData = Ints.toByteArray(0);
|
||||
return this.cachedData;
|
||||
}
|
||||
|
||||
// How many of each timestamp
|
||||
Map<Long, Integer> countByTimestamp = new HashMap<>();
|
||||
|
||||
for (TradePresenceData tradePresenceData : this.tradePresences) {
|
||||
Long timestamp = tradePresenceData.getTimestamp();
|
||||
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
|
||||
+ this.tradePresences.size() * Transformer.PUBLIC_KEY_LENGTH;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
for (long timestamp : countByTimestamp.keySet()) {
|
||||
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
for (TradePresenceData tradePresenceData : this.tradePresences) {
|
||||
if (tradePresenceData.getTimestamp() == timestamp)
|
||||
bytes.write(tradePresenceData.getPublicKey());
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedData = bytes.toByteArray();
|
||||
return this.cachedData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
public class GetTransactionMessage extends Message {
|
||||
|
||||
private static final int TRANSACTION_SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private byte[] signature;
|
||||
|
||||
public GetTransactionMessage(byte[] signature) {
|
||||
this(-1, signature);
|
||||
super(MessageType.GET_TRANSACTION);
|
||||
|
||||
this.dataBytes = Arrays.copyOf(signature, signature.length);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetTransactionMessage(int id, byte[] signature) {
|
||||
@@ -27,28 +26,12 @@ public class GetTransactionMessage extends Message {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != TRANSACTION_SIGNATURE_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] signature = new byte[TRANSACTION_SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
|
||||
bytes.get(signature);
|
||||
|
||||
return new GetTransactionMessage(id, signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetUnconfirmedTransactionsMessage extends Message {
|
||||
|
||||
public GetUnconfirmedTransactionsMessage() {
|
||||
this(-1);
|
||||
super(MessageType.GET_UNCONFIRMED_TRANSACTIONS);
|
||||
|
||||
this.dataBytes = EMPTY_DATA_BYTES;
|
||||
}
|
||||
|
||||
private GetUnconfirmedTransactionsMessage(int id) {
|
||||
super(id, MessageType.GET_UNCONFIRMED_TRANSACTIONS);
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
return new GetUnconfirmedTransactionsMessage(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.qortal.network.message;
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -22,7 +21,7 @@ public class GoodbyeMessage extends Message {
|
||||
private static final Map<Integer, Reason> map = stream(Reason.values())
|
||||
.collect(toMap(reason -> reason.value, reason -> reason));
|
||||
|
||||
private Reason(int value) {
|
||||
Reason(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@@ -31,7 +30,14 @@ public class GoodbyeMessage extends Message {
|
||||
}
|
||||
}
|
||||
|
||||
private final Reason reason;
|
||||
private Reason reason;
|
||||
|
||||
public GoodbyeMessage(Reason reason) {
|
||||
super(MessageType.GOODBYE);
|
||||
|
||||
this.dataBytes = Ints.toByteArray(reason.value);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GoodbyeMessage(int id, Reason reason) {
|
||||
super(id, MessageType.GOODBYE);
|
||||
@@ -39,27 +45,18 @@ public class GoodbyeMessage extends Message {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public GoodbyeMessage(Reason reason) {
|
||||
this(-1, reason);
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return this.reason;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
int reasonValue = byteBuffer.getInt();
|
||||
|
||||
Reason reason = Reason.valueOf(reasonValue);
|
||||
if (reason == null)
|
||||
return null;
|
||||
throw new MessageException("Invalid reason " + reasonValue + " in GOODBYE message");
|
||||
|
||||
return new GoodbyeMessage(id, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() throws IOException {
|
||||
return Ints.toByteArray(this.reason.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
@@ -19,7 +18,24 @@ public class HeightV2Message extends Message {
|
||||
private byte[] minterPublicKey;
|
||||
|
||||
public HeightV2Message(int height, byte[] signature, long timestamp, byte[] minterPublicKey) {
|
||||
this(-1, height, signature, timestamp, minterPublicKey);
|
||||
super(MessageType.HEIGHT_V2);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(height));
|
||||
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
bytes.write(minterPublicKey);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private HeightV2Message(int id, int height, byte[] signature, long timestamp, byte[] minterPublicKey) {
|
||||
@@ -47,7 +63,7 @@ public class HeightV2Message extends Message {
|
||||
return this.minterPublicKey;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
int height = bytes.getInt();
|
||||
|
||||
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
|
||||
@@ -61,23 +77,4 @@ public class HeightV2Message extends Message {
|
||||
return new HeightV2Message(id, height, signature, timestamp, minterPublicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.height));
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(this.timestamp));
|
||||
|
||||
bytes.write(this.minterPublicKey);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,9 +11,28 @@ import com.google.common.primitives.Longs;
|
||||
|
||||
public class HelloMessage extends Message {
|
||||
|
||||
private final long timestamp;
|
||||
private final String versionString;
|
||||
private final String senderPeerAddress;
|
||||
private long timestamp;
|
||||
private String versionString;
|
||||
private String senderPeerAddress;
|
||||
|
||||
public HelloMessage(long timestamp, String versionString, String senderPeerAddress) {
|
||||
super(MessageType.HELLO);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
Serialization.serializeSizedString(bytes, versionString);
|
||||
|
||||
Serialization.serializeSizedString(bytes, senderPeerAddress);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private HelloMessage(int id, long timestamp, String versionString, String senderPeerAddress) {
|
||||
super(id, MessageType.HELLO);
|
||||
@@ -23,10 +42,6 @@ public class HelloMessage extends Message {
|
||||
this.senderPeerAddress = senderPeerAddress;
|
||||
}
|
||||
|
||||
public HelloMessage(long timestamp, String versionString, String senderPeerAddress) {
|
||||
this(-1, timestamp, versionString, senderPeerAddress);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
@@ -39,31 +54,23 @@ public class HelloMessage extends Message {
|
||||
return this.senderPeerAddress;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws TransformationException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
long timestamp = byteBuffer.getLong();
|
||||
|
||||
String versionString = Serialization.deserializeSizedString(byteBuffer, 255);
|
||||
|
||||
// Sender peer address added in v3.0, so is an optional field. Older versions won't send it.
|
||||
String versionString;
|
||||
String senderPeerAddress = null;
|
||||
if (byteBuffer.hasRemaining()) {
|
||||
senderPeerAddress = Serialization.deserializeSizedString(byteBuffer, 255);
|
||||
try {
|
||||
versionString = Serialization.deserializeSizedString(byteBuffer, 255);
|
||||
|
||||
// Sender peer address added in v3.0, so is an optional field. Older versions won't send it.
|
||||
if (byteBuffer.hasRemaining()) {
|
||||
senderPeerAddress = Serialization.deserializeSizedString(byteBuffer, 255);
|
||||
}
|
||||
} catch (TransformationException e) {
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return new HelloMessage(id, timestamp, versionString, senderPeerAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Longs.toByteArray(this.timestamp));
|
||||
|
||||
Serialization.serializeSizedString(bytes, this.versionString);
|
||||
|
||||
Serialization.serializeSizedString(bytes, this.senderPeerAddress);
|
||||
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,161 +1,68 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Network message for sending over network, or unpacked data received from network.
|
||||
* <p></p>
|
||||
* <p>
|
||||
* For messages received from network, subclass's {@code fromByteBuffer()} method is used
|
||||
* to construct a subclassed instance. Original bytes from network are not retained.
|
||||
* Access to deserialized data should be via subclass's getters. Ideally there should be NO setters!
|
||||
* </p>
|
||||
* <p></p>
|
||||
* <p>
|
||||
* Each subclass's <b>public</b> constructor is for building a message to send <b>only</b>.
|
||||
* The constructor will serialize into byte form but <b>not</b> store the passed args.
|
||||
* Serialized bytes are saved into superclass (Message) {@code dataBytes} and, if not empty,
|
||||
* a checksum is created and saved into {@code checksumBytes}.
|
||||
* Therefore: <i>do not use subclass's getters after using constructor!</i>
|
||||
* </p>
|
||||
* <p></p>
|
||||
* <p>
|
||||
* For subclasses where outgoing versions might be usefully cached, they can implement Clonable
|
||||
* as long if they are safe to use {@link Object#clone()}.
|
||||
* </p>
|
||||
*/
|
||||
public abstract class Message {
|
||||
|
||||
// MAGIC(4) + TYPE(4) + HAS-ID(1) + ID?(4) + DATA-SIZE(4) + CHECKSUM?(4) + DATA?(*)
|
||||
private static final int MAGIC_LENGTH = 4;
|
||||
private static final int TYPE_LENGTH = 4;
|
||||
private static final int HAS_ID_LENGTH = 1;
|
||||
private static final int ID_LENGTH = 4;
|
||||
private static final int DATA_SIZE_LENGTH = 4;
|
||||
private static final int CHECKSUM_LENGTH = 4;
|
||||
|
||||
private static final int MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class MessageException extends Exception {
|
||||
public MessageException() {
|
||||
}
|
||||
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();
|
||||
|
||||
public MessageException(String message) {
|
||||
super(message);
|
||||
}
|
||||
protected int id;
|
||||
protected final MessageType type;
|
||||
|
||||
public MessageException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public MessageException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageType {
|
||||
// Handshaking
|
||||
HELLO(0),
|
||||
GOODBYE(1),
|
||||
CHALLENGE(2),
|
||||
RESPONSE(3),
|
||||
|
||||
// Status / notifications
|
||||
HEIGHT_V2(10),
|
||||
PING(11),
|
||||
PONG(12),
|
||||
|
||||
// Requesting data
|
||||
PEERS_V2(20),
|
||||
GET_PEERS(21),
|
||||
|
||||
TRANSACTION(30),
|
||||
GET_TRANSACTION(31),
|
||||
|
||||
TRANSACTION_SIGNATURES(40),
|
||||
GET_UNCONFIRMED_TRANSACTIONS(41),
|
||||
|
||||
BLOCK(50),
|
||||
GET_BLOCK(51),
|
||||
|
||||
SIGNATURES(60),
|
||||
GET_SIGNATURES_V2(61),
|
||||
|
||||
BLOCK_SUMMARIES(70),
|
||||
GET_BLOCK_SUMMARIES(71),
|
||||
|
||||
ONLINE_ACCOUNTS(80),
|
||||
GET_ONLINE_ACCOUNTS(81),
|
||||
ONLINE_ACCOUNTS_V2(82),
|
||||
GET_ONLINE_ACCOUNTS_V2(83),
|
||||
|
||||
ARBITRARY_DATA(90),
|
||||
GET_ARBITRARY_DATA(91),
|
||||
|
||||
BLOCKS(100),
|
||||
GET_BLOCKS(101),
|
||||
|
||||
ARBITRARY_DATA_FILE(110),
|
||||
GET_ARBITRARY_DATA_FILE(111),
|
||||
|
||||
ARBITRARY_DATA_FILE_LIST(120),
|
||||
GET_ARBITRARY_DATA_FILE_LIST(121),
|
||||
|
||||
ARBITRARY_SIGNATURES(130),
|
||||
|
||||
TRADE_PRESENCES(140),
|
||||
GET_TRADE_PRESENCES(141),
|
||||
|
||||
ARBITRARY_METADATA(150),
|
||||
GET_ARBITRARY_METADATA(151);
|
||||
|
||||
public final int value;
|
||||
public final Method fromByteBufferMethod;
|
||||
|
||||
private static final Map<Integer, MessageType> map = stream(MessageType.values())
|
||||
.collect(toMap(messageType -> messageType.value, messageType -> messageType));
|
||||
|
||||
private MessageType(int value) {
|
||||
this.value = value;
|
||||
|
||||
String[] classNameParts = this.name().toLowerCase().split("_");
|
||||
|
||||
for (int i = 0; i < classNameParts.length; ++i)
|
||||
classNameParts[i] = classNameParts[i].substring(0, 1).toUpperCase().concat(classNameParts[i].substring(1));
|
||||
|
||||
String className = String.join("", classNameParts);
|
||||
|
||||
Method method;
|
||||
try {
|
||||
Class<?> subclass = Class.forName(String.join("", Message.class.getPackage().getName(), ".", className, "Message"));
|
||||
|
||||
method = subclass.getDeclaredMethod("fromByteBuffer", int.class, ByteBuffer.class);
|
||||
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
|
||||
method = null;
|
||||
}
|
||||
|
||||
this.fromByteBufferMethod = method;
|
||||
}
|
||||
|
||||
public static MessageType valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
public Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
if (this.fromByteBufferMethod == null)
|
||||
throw new MessageException("Unsupported message type [" + value + "] during conversion from bytes");
|
||||
|
||||
try {
|
||||
return (Message) this.fromByteBufferMethod.invoke(null, id, byteBuffer);
|
||||
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||
if (e.getCause() instanceof BufferUnderflowException)
|
||||
throw new MessageException("Byte data too short for " + name() + " message");
|
||||
|
||||
throw new MessageException("Internal error with " + name() + " message during conversion from bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int id;
|
||||
private MessageType type;
|
||||
/** Serialized outgoing message data. Expected to be written to by subclass. */
|
||||
protected byte[] dataBytes;
|
||||
/** Serialized outgoing message checksum. Expected to be written to by subclass. */
|
||||
protected byte[] checksumBytes;
|
||||
|
||||
/** Typically called by subclass when constructing message from received network data. */
|
||||
protected Message(int id, MessageType type) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/** Typically called by subclass when constructing outgoing message. */
|
||||
protected Message(MessageType type) {
|
||||
this(-1, type);
|
||||
}
|
||||
@@ -179,9 +86,9 @@ public abstract class Message {
|
||||
/**
|
||||
* Attempt to read a message from byte buffer.
|
||||
*
|
||||
* @param readOnlyBuffer
|
||||
* @param readOnlyBuffer ByteBuffer containing bytes read from network
|
||||
* @return null if no complete message can be read
|
||||
* @throws MessageException
|
||||
* @throws MessageException if message could not be decoded or is invalid
|
||||
*/
|
||||
public static Message fromByteBuffer(ByteBuffer readOnlyBuffer) throws MessageException {
|
||||
try {
|
||||
@@ -197,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();
|
||||
@@ -221,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);
|
||||
@@ -256,9 +162,27 @@ public abstract class Message {
|
||||
return Arrays.copyOfRange(Crypto.digest(dataBuffer), 0, CHECKSUM_LENGTH);
|
||||
}
|
||||
|
||||
public void checkValidOutgoing() throws MessageException {
|
||||
// We expect subclass to have initialized these
|
||||
if (this.dataBytes == null)
|
||||
throw new MessageException("Missing data payload");
|
||||
if (this.dataBytes.length > 0 && this.checksumBytes == null)
|
||||
throw new MessageException("Missing data checksum");
|
||||
}
|
||||
|
||||
public byte[] toBytes() throws MessageException {
|
||||
checkValidOutgoing();
|
||||
|
||||
// We can calculate exact length
|
||||
int messageLength = MAGIC_LENGTH + TYPE_LENGTH + HAS_ID_LENGTH;
|
||||
messageLength += this.hasId() ? ID_LENGTH : 0;
|
||||
messageLength += DATA_SIZE_LENGTH + this.dataBytes.length > 0 ? CHECKSUM_LENGTH + this.dataBytes.length : 0;
|
||||
|
||||
if (messageLength > MAX_DATA_SIZE)
|
||||
throw new MessageException(String.format("About to send message with length %d larger than allowed %d", messageLength, MAX_DATA_SIZE));
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(messageLength);
|
||||
|
||||
// Magic
|
||||
bytes.write(Network.getInstance().getMessageMagic());
|
||||
@@ -273,26 +197,30 @@ public abstract class Message {
|
||||
bytes.write(0);
|
||||
}
|
||||
|
||||
byte[] data = this.toData();
|
||||
if (data == null)
|
||||
throw new MessageException("Missing data payload");
|
||||
bytes.write(Ints.toByteArray(this.dataBytes.length));
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
if (data.length > 0) {
|
||||
bytes.write(generateChecksum(data));
|
||||
bytes.write(data);
|
||||
if (this.dataBytes.length > 0) {
|
||||
bytes.write(this.checksumBytes);
|
||||
bytes.write(this.dataBytes);
|
||||
}
|
||||
|
||||
if (bytes.size() > MAX_DATA_SIZE)
|
||||
throw new MessageException(String.format("About to send message with length %d larger than allowed %d", bytes.size(), MAX_DATA_SIZE));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException | TransformationException e) {
|
||||
} catch (IOException e) {
|
||||
throw new MessageException("Failed to serialize message", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract byte[] toData() throws IOException, TransformationException;
|
||||
public static <M extends Message> M cloneWithNewId(M message, int newId) {
|
||||
M clone;
|
||||
|
||||
try {
|
||||
clone = (M) message.clone();
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new UnsupportedOperationException("Message sub-class not cloneable");
|
||||
}
|
||||
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class MessageException extends Exception {
|
||||
public MessageException() {
|
||||
}
|
||||
|
||||
public MessageException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MessageException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public MessageException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface MessageProducer {
|
||||
Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user