forked from Qortal/qortal
Merge branch 'master' into german
This commit is contained in:
commit
42f2d015b7
44
TestNets.md
44
TestNets.md
@ -52,14 +52,13 @@
|
|||||||
|
|
||||||
## Single-node testnet
|
## Single-node testnet
|
||||||
|
|
||||||
A single-node testnet is possible with code modifications, for basic testing, or to more easily start a new testnet.
|
A single-node testnet is possible with an additional settings, or to more easily start a new testnet.
|
||||||
To do so, follow these steps:
|
Just add this setting:
|
||||||
- Comment out the `if (mintedLastBlock) { }` conditional in BlockMinter.java
|
```
|
||||||
- Comment out the `minBlockchainPeers` validation in Settings.validate()
|
"singleNodeTestnet": true
|
||||||
- Set `minBlockchainPeers` to 0 in settings.json
|
```
|
||||||
- Set `Synchronizer.RECOVERY_MODE_TIMEOUT` to `0`
|
This will automatically allow multiple consecutive blocks to be minted, as well as setting minBlockchainPeers to 0.
|
||||||
- All other steps should remain the same. Only a single reward share key is needed.
|
Remember to put these values back after introducing other nodes
|
||||||
- Remember to put these values back after introducing other nodes
|
|
||||||
|
|
||||||
## Fixed network
|
## Fixed network
|
||||||
|
|
||||||
@ -93,3 +92,32 @@ Your options are:
|
|||||||
- `qort` tool, but prepend with one-time shell variable: `BASE_URL=some-node-hostname-or-ip:port qort ......`
|
- `qort` tool, but prepend with one-time shell variable: `BASE_URL=some-node-hostname-or-ip:port qort ......`
|
||||||
- `peer-heights`, but use `-t` option, or `BASE_URL` shell variable as above
|
- `peer-heights`, but use `-t` option, or `BASE_URL` shell variable as above
|
||||||
|
|
||||||
|
## Example settings-test.json
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"isTestNet": true,
|
||||||
|
"bitcoinNet": "TEST3",
|
||||||
|
"repositoryPath": "db-testnet",
|
||||||
|
"blockchainConfig": "testchain.json",
|
||||||
|
"minBlockchainPeers": 1,
|
||||||
|
"apiDocumentationEnabled": true,
|
||||||
|
"apiRestricted": false,
|
||||||
|
"bootstrap": false,
|
||||||
|
"maxPeerConnectionTime": 999999999,
|
||||||
|
"localAuthBypassEnabled": true,
|
||||||
|
"singleNodeTestnet": true,
|
||||||
|
"recoveryModeTimeout": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
Here are some steps to quickly get a single node testnet up and running with a generic minting account:
|
||||||
|
1. Start with template `settings-test.json`, and create a `testchain.json` based on mainnet's blockchain.json (or obtain one from Qortal developers). These should be in the same directory as the jar.
|
||||||
|
2. Make sure feature triggers and other timestamp/height activations are correctly set. Generally these would be `0` so that they are enabled from the start.
|
||||||
|
3. Set a recent genesis `timestamp` in testchain.json, and add this reward share entry:
|
||||||
|
`{ "type": "REWARD_SHARE", "minterPublicKey": "DwcUnhxjamqppgfXCLgbYRx8H9XFPUc2qYRy3CEvQWEw", "recipient": "QbTDMss7NtRxxQaSqBZtSLSNdSYgvGaqFf", "rewardSharePublicKey": "CRvQXxFfUMfr4q3o1PcUZPA4aPCiubBsXkk47GzRo754", "sharePercent": 0 },`
|
||||||
|
4. Start the node, passing in settings-test.json, e.g: `java -jar qortal.jar settings-test.json`
|
||||||
|
5. Once started, add the corresponding minting key to the node:
|
||||||
|
`curl -X POST "http://localhost:62391/admin/mintingaccounts" -d "F48mYJycFgRdqtc58kiovwbcJgVukjzRE4qRRtRsK9ix"`
|
||||||
|
6. Alternatively you can use your own minting account instead of the generic one above.
|
||||||
|
7. After a short while, blocks should be minted from the genesis timestamp until the current time.
|
@ -17,10 +17,10 @@
|
|||||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||||
<ROW Property="NTP_GOOD" Value="false"/>
|
<ROW Property="NTP_GOOD" Value="false"/>
|
||||||
<ROW Property="ProductCode" Value="1033:{0D69BF4B-1E5B-4CA8-87B4-EB4FF79B25C3} 1049:{4F5AA833-0C13-4F77-882F-150DE9AB1CC9} 2052:{9C1AC280-B306-4119-8BA7-FFF88BE7D434} 2057:{11A05F71-8A35-4A60-A4D2-A46EC63AF413} " Type="16"/>
|
<ROW Property="ProductCode" Value="1033:{6C93A96C-E3AF-42FD-BE11-7EC3734905C6} 1049:{754F5347-82E5-4251-AED0-F4141CDD11F5} 2052:{413BD7B3-A3F8-47D0-BCA4-5C7694A40936} 2057:{71450AC8-1E6F-4469-852D-0591FA693680} " Type="16"/>
|
||||||
<ROW Property="ProductLanguage" Value="2057"/>
|
<ROW Property="ProductLanguage" Value="2057"/>
|
||||||
<ROW Property="ProductName" Value="Qortal"/>
|
<ROW Property="ProductName" Value="Qortal"/>
|
||||||
<ROW Property="ProductVersion" Value="3.3.7" Type="32"/>
|
<ROW Property="ProductVersion" Value="3.8.3" Type="32"/>
|
||||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
<ROW Property="REPAIR_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_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_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="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="{2DD67E39-9AFB-4821-AE3E-6186A20826B2}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
<ROW Component="AI_CustomARPName" ComponentId="{EC7B4AD9-F2D9-48C4-A586-C4697D9C380C}" 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="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="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"/>
|
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
||||||
@ -1173,7 +1173,7 @@
|
|||||||
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
|
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
|
||||||
<ROW Action="AI_SetPermissions" Type="11265" Source="userAccounts.dll" Target="OnSetPermissions" WithoutSeq="true"/>
|
<ROW Action="AI_SetPermissions" Type="11265" Source="userAccounts.dll" Target="OnSetPermissions" WithoutSeq="true"/>
|
||||||
<ROW Action="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Make copy fso.CopyFile(appDir + "log4j2.properties", appDir + "log4j2-orig.properties", true); // overwrite // Rewrite %AppDir%\log4j2.properties to update logfile storage path var fin = fso.OpenTextFile(appDir + "log4j2-orig.properties", ForReading, false); // no create var fout = fso.OpenTextFile(appDir + "log4j2.properties", ForWriting, true); // can create // Copy lines with rewriting where necessary while( !fin.AtEndOfStream ) { 	var line = fin.ReadLine(); 	var start = line.indexOf("property.dirname"); 	if (start > 0) { 		// line: # property.dirname = ...appdata... 		// uncomment/replace this line for Windows 		fout.WriteLine( "property.dirname = " + dataFolder.split('\\').join('\\\\') ); 	} else { 		// not found - output verbatim 		fout.WriteLine( line ); 	} } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_4"/>
|
<ROW Action="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Make copy fso.CopyFile(appDir + "log4j2.properties", appDir + "log4j2-orig.properties", true); // overwrite // Rewrite %AppDir%\log4j2.properties to update logfile storage path var fin = fso.OpenTextFile(appDir + "log4j2-orig.properties", ForReading, false); // no create var fout = fso.OpenTextFile(appDir + "log4j2.properties", ForWriting, true); // can create // Copy lines with rewriting where necessary while( !fin.AtEndOfStream ) { 	var line = fin.ReadLine(); 	var start = line.indexOf("property.dirname"); 	if (start > 0) { 		// line: # property.dirname = ...appdata... 		// uncomment/replace this line for Windows 		fout.WriteLine( "property.dirname = " + dataFolder.split('\\').join('\\\\') ); 	} else { 		// not found - output verbatim 		fout.WriteLine( line ); 	} } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_4"/>
|
||||||
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder var fts = fso.OpenTextFile(appDir + "settings.json", ForWriting, true); fts.WriteLine( "{" ); // We need to escape Windows path backslashes to keep JSON valid fts.WriteLine( " \"userPath\": \"" + dataFolder.split('\\').join('\\\\') + "\"" ); fts.WriteLine( "}" ); fts.Close(); // Make copy fso.CopyFile(dataFolder + "settings.json", dataFolder + "settings-orig.json", true); // overwrite // Rewrite settings.json to update repository path var fin = fso.OpenTextFile(dataFolder + "settings-orig.json", ForReading, false); var fout = fso.OpenTextFile(dataFolder + "settings.json", ForWriting, true); // First line should contain opening brace fout.WriteLine( fin.ReadLine() ); // Append our entries fout.WriteLine( " \"repositoryPath\": \"" + dataFolder.split('\\').join('\\\\') + "db\"," ); fout.WriteLine( " \"dataPath\": \"" + dataFolder.split('\\').join('\\\\') + "data\"," ); // copy rest of settings while( !fin.AtEndOfStream ) { 	fout.WriteLine( fin.ReadLine() ); } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_3"/>
|
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder var fts = fso.OpenTextFile(appDir + "settings.json", ForWriting, true); fts.WriteLine( "{" ); // We need to escape Windows path backslashes to keep JSON valid fts.WriteLine( " \"userPath\": \"" + dataFolder.split('\\').join('\\\\') + "\"" ); fts.WriteLine( "}" ); fts.Close(); // Make copy fso.CopyFile(dataFolder + "settings.json", dataFolder + "settings-orig.json", true); // overwrite // Rewrite settings.json to update repository path var fin = fso.OpenTextFile(dataFolder + "settings-orig.json", ForReading, false); var fout = fso.OpenTextFile(dataFolder + "settings.json", ForWriting, true); // First line should contain opening brace fout.WriteLine( fin.ReadLine() ); // Append our entries fout.WriteLine( " \"repositoryPath\": \"" + dataFolder.split('\\').join('\\\\') + "db\"," ); fout.WriteLine( " \"dataPath\": \"" + dataFolder.split('\\').join('\\\\') + "data\"," ); fout.WriteLine( " \"walletsPath\": \"" + dataFolder.split('\\').join('\\\\') + "wallets\"," ); fout.WriteLine( " \"listsPath\": \"" + dataFolder.split('\\').join('\\\\') + "lists\"," ); // copy rest of settings while( !fin.AtEndOfStream ) { 	fout.WriteLine( fin.ReadLine() ); } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_3"/>
|
||||||
<ROW Action="DetectRunningProcess" Type="1" Source="aicustact.dll" Target="DetectProcess" Options="3" AdditionalSeq="AI_DATA_SETTER_8"/>
|
<ROW Action="DetectRunningProcess" Type="1" Source="aicustact.dll" Target="DetectProcess" Options="3" AdditionalSeq="AI_DATA_SETTER_8"/>
|
||||||
<ROW Action="DetectW32Time" Type="1" Source="aicustact.dll" Target="DetectService" Options="3" AdditionalSeq="AI_DATA_SETTER_11"/>
|
<ROW Action="DetectW32Time" Type="1" Source="aicustact.dll" Target="DetectService" Options="3" AdditionalSeq="AI_DATA_SETTER_11"/>
|
||||||
<ROW Action="NTP_config" Type="3090" Source="ntpcfg.bat"/>
|
<ROW Action="NTP_config" Type="3090" Source="ntpcfg.bat"/>
|
||||||
|
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>org.ciyam</groupId>
|
||||||
|
<artifactId>AT</artifactId>
|
||||||
|
<version>1.4.0</version>
|
||||||
|
<description>POM was created from install:install-file</description>
|
||||||
|
</project>
|
@ -3,14 +3,15 @@
|
|||||||
<groupId>org.ciyam</groupId>
|
<groupId>org.ciyam</groupId>
|
||||||
<artifactId>AT</artifactId>
|
<artifactId>AT</artifactId>
|
||||||
<versioning>
|
<versioning>
|
||||||
<release>1.3.8</release>
|
<release>1.4.0</release>
|
||||||
<versions>
|
<versions>
|
||||||
<version>1.3.4</version>
|
<version>1.3.4</version>
|
||||||
<version>1.3.5</version>
|
<version>1.3.5</version>
|
||||||
<version>1.3.6</version>
|
<version>1.3.6</version>
|
||||||
<version>1.3.7</version>
|
<version>1.3.7</version>
|
||||||
<version>1.3.8</version>
|
<version>1.3.8</version>
|
||||||
|
<version>1.4.0</version>
|
||||||
</versions>
|
</versions>
|
||||||
<lastUpdated>20200925114415</lastUpdated>
|
<lastUpdated>20221105114346</lastUpdated>
|
||||||
</versioning>
|
</versioning>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
30
pom.xml
30
pom.xml
@ -3,15 +3,15 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.qortal</groupId>
|
<groupId>org.qortal</groupId>
|
||||||
<artifactId>qortal</artifactId>
|
<artifactId>qortal</artifactId>
|
||||||
<version>3.3.7</version>
|
<version>3.8.4</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
<altcoinj.version>6628cfd</altcoinj.version>
|
<altcoinj.version>7dc8c6f</altcoinj.version>
|
||||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||||
<bouncycastle.version>1.64</bouncycastle.version>
|
<bouncycastle.version>1.69</bouncycastle.version>
|
||||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||||
<ciyam-at.version>1.3.8</ciyam-at.version>
|
<ciyam-at.version>1.4.0</ciyam-at.version>
|
||||||
<commons-net.version>3.6</commons-net.version>
|
<commons-net.version>3.6</commons-net.version>
|
||||||
<commons-text.version>1.8</commons-text.version>
|
<commons-text.version>1.8</commons-text.version>
|
||||||
<commons-io.version>2.6</commons-io.version>
|
<commons-io.version>2.6</commons-io.version>
|
||||||
@ -34,6 +34,8 @@
|
|||||||
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
|
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
|
||||||
<jsoup.version>1.13.1</jsoup.version>
|
<jsoup.version>1.13.1</jsoup.version>
|
||||||
<java-diff-utils.version>4.10</java-diff-utils.version>
|
<java-diff-utils.version>4.10</java-diff-utils.version>
|
||||||
|
<grpc.version>1.45.1</grpc.version>
|
||||||
|
<protobuf.version>3.19.4</protobuf.version>
|
||||||
</properties>
|
</properties>
|
||||||
<build>
|
<build>
|
||||||
<sourceDirectory>src/main/java</sourceDirectory>
|
<sourceDirectory>src/main/java</sourceDirectory>
|
||||||
@ -705,5 +707,25 @@
|
|||||||
<artifactId>java-diff-utils</artifactId>
|
<artifactId>java-diff-utils</artifactId>
|
||||||
<version>${java-diff-utils.version}</version>
|
<version>${java-diff-utils.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-netty</artifactId>
|
||||||
|
<version>${grpc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-protobuf</artifactId>
|
||||||
|
<version>${grpc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-stub</artifactId>
|
||||||
|
<version>${grpc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.protobuf</groupId>
|
||||||
|
<artifactId>protobuf-java</artifactId>
|
||||||
|
<version>${protobuf.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
4499
src/main/java/cash/z/wallet/sdk/rpc/CompactFormats.java
Normal file
4499
src/main/java/cash/z/wallet/sdk/rpc/CompactFormats.java
Normal file
File diff suppressed because it is too large
Load Diff
1341
src/main/java/cash/z/wallet/sdk/rpc/CompactTxStreamerGrpc.java
Normal file
1341
src/main/java/cash/z/wallet/sdk/rpc/CompactTxStreamerGrpc.java
Normal file
File diff suppressed because it is too large
Load Diff
3854
src/main/java/cash/z/wallet/sdk/rpc/Darkside.java
Normal file
3854
src/main/java/cash/z/wallet/sdk/rpc/Darkside.java
Normal file
File diff suppressed because it is too large
Load Diff
1086
src/main/java/cash/z/wallet/sdk/rpc/DarksideStreamerGrpc.java
Normal file
1086
src/main/java/cash/z/wallet/sdk/rpc/DarksideStreamerGrpc.java
Normal file
File diff suppressed because it is too large
Load Diff
15106
src/main/java/cash/z/wallet/sdk/rpc/Service.java
Normal file
15106
src/main/java/cash/z/wallet/sdk/rpc/Service.java
Normal file
File diff suppressed because it is too large
Load Diff
100
src/main/java/com/rust/litewalletjni/LiteWalletJni.java
Normal file
100
src/main/java/com/rust/litewalletjni/LiteWalletJni.java
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* LiteWalletJni code based on https://github.com/PirateNetwork/cordova-plugin-litewallet
|
||||||
|
*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020 Zero Currency Coin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.rust.litewalletjni;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.controller.PirateChainWalletController;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
public class LiteWalletJni {
|
||||||
|
|
||||||
|
protected static final Logger LOGGER = LogManager.getLogger(LiteWalletJni.class);
|
||||||
|
|
||||||
|
public static native String initlogging();
|
||||||
|
public static native String initnew(final String serveruri, final String params, final String saplingOutputb64, final String saplingSpendb64);
|
||||||
|
public static native String initfromseed(final String serveruri, final String params, final String seed, final String birthday, final String saplingOutputb64, final String saplingSpendb64);
|
||||||
|
public static native String initfromb64(final String serveruri, final String params, final String datab64, final String saplingOutputb64, final String saplingSpendb64);
|
||||||
|
public static native String save();
|
||||||
|
|
||||||
|
public static native String execute(final String cmd, final String args);
|
||||||
|
public static native String getseedphrase();
|
||||||
|
public static native String getseedphrasefromentropyb64(final String entropy64);
|
||||||
|
public static native String checkseedphrase(final String input);
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean loaded = false;
|
||||||
|
|
||||||
|
public static void loadLibrary() {
|
||||||
|
if (loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String osName = System.getProperty("os.name");
|
||||||
|
String osArchitecture = System.getProperty("os.arch");
|
||||||
|
|
||||||
|
LOGGER.info("OS Name: {}", osName);
|
||||||
|
LOGGER.info("OS Architecture: {}", osArchitecture);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String libFileName = PirateChainWalletController.getRustLibFilename();
|
||||||
|
if (libFileName == null) {
|
||||||
|
LOGGER.info("Library not found for OS: {}, arch: {}", osName, osArchitecture);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path libPath = Paths.get(PirateChainWalletController.getRustLibOuterDirectory().toString(), libFileName);
|
||||||
|
System.load(libPath.toAbsolutePath().toString());
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
catch (UnsatisfiedLinkError e) {
|
||||||
|
LOGGER.info("Unable to load library");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLoaded() {
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,6 +8,7 @@ import java.nio.file.Paths;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -18,6 +19,8 @@ import org.qortal.api.ApiRequest;
|
|||||||
import org.qortal.controller.AutoUpdate;
|
import org.qortal.controller.AutoUpdate;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
|
import static org.qortal.controller.AutoUpdate.AGENTLIB_JVM_HOLDER_ARG;
|
||||||
|
|
||||||
public class ApplyUpdate {
|
public class ApplyUpdate {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@ -197,6 +200,11 @@ public class ApplyUpdate {
|
|||||||
// JVM arguments
|
// JVM arguments
|
||||||
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||||
|
|
||||||
|
// Reapply any retained, but disabled, -agentlib JVM arg
|
||||||
|
javaCmd = javaCmd.stream()
|
||||||
|
.map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// Call mainClass in JAR
|
// Call mainClass in JAR
|
||||||
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
|
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
|
||||||
|
|
||||||
@ -205,7 +213,7 @@ public class ApplyUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LOGGER.info(() -> String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||||
|
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||||
|
|
||||||
@ -214,8 +222,15 @@ public class ApplyUpdate {
|
|||||||
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
processBuilder.start();
|
// New process will inherit our stdout and stderr
|
||||||
} catch (IOException e) {
|
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
||||||
|
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
|
||||||
|
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
|
||||||
|
// Nothing to pipe to new process, so close output stream (process's stdin)
|
||||||
|
process.getOutputStream().close();
|
||||||
|
} catch (Exception e) {
|
||||||
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
|
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,7 +211,8 @@ public class Account {
|
|||||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (Account.isFounder(accountData.getFlags()))
|
// Founders can always mint, unless they have a penalty
|
||||||
|
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -222,6 +223,11 @@ public class Account {
|
|||||||
return this.repository.getAccountRepository().getMintedBlockCount(this.address);
|
return this.repository.getAccountRepository().getMintedBlockCount(this.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns account's blockMintedPenalty or null if account not found in repository. */
|
||||||
|
public Integer getBlocksMintedPenalty() throws DataException {
|
||||||
|
return this.repository.getAccountRepository().getBlocksMintedPenaltyCount(this.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Returns whether account can build reward-shares.
|
/** Returns whether account can build reward-shares.
|
||||||
* <p>
|
* <p>
|
||||||
@ -243,7 +249,7 @@ public class Account {
|
|||||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
|
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (Account.isFounder(accountData.getFlags()))
|
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -271,7 +277,7 @@ public class Account {
|
|||||||
/**
|
/**
|
||||||
* Returns 'effective' minting level, or zero if account does not exist/cannot mint.
|
* Returns 'effective' minting level, or zero if account does not exist/cannot mint.
|
||||||
* <p>
|
* <p>
|
||||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
* For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||||
*
|
*
|
||||||
* @return 0+
|
* @return 0+
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
@ -281,7 +287,8 @@ public class Account {
|
|||||||
if (accountData == null)
|
if (accountData == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (Account.isFounder(accountData.getFlags()))
|
// Founders are assigned a different effective minting level, as long as they have no penalty
|
||||||
|
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||||
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
||||||
|
|
||||||
return accountData.getLevel();
|
return accountData.getLevel();
|
||||||
@ -289,8 +296,6 @@ public class Account {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
||||||
* <p>
|
|
||||||
* this is being used on src/main/java/org/qortal/api/resource/AddressesResource.java to fulfil the online accounts api call
|
|
||||||
*
|
*
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param rewardSharePublicKey
|
* @param rewardSharePublicKey
|
||||||
@ -309,7 +314,7 @@ public class Account {
|
|||||||
/**
|
/**
|
||||||
* Returns 'effective' minting level, with a fix for the zero level.
|
* Returns 'effective' minting level, with a fix for the zero level.
|
||||||
* <p>
|
* <p>
|
||||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
* For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||||
*
|
*
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param rewardSharePublicKey
|
* @param rewardSharePublicKey
|
||||||
@ -322,7 +327,7 @@ public class Account {
|
|||||||
if (rewardShareData == null)
|
if (rewardShareData == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
else if(!rewardShareData.getMinter().equals(rewardShareData.getRecipient()))//the minter is different than the recipient this means sponsorship
|
else if (!rewardShareData.getMinter().equals(rewardShareData.getRecipient())) // Sponsorship reward share
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||||
|
@ -11,15 +11,15 @@ public class PrivateKeyAccount extends PublicKeyAccount {
|
|||||||
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
|
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
|
* byte[32] used to create private/public key pair
|
||||||
* @throws IllegalArgumentException
|
* @throws IllegalArgumentException
|
||||||
* if passed invalid seed
|
* if passed invalid privateKey
|
||||||
*/
|
*/
|
||||||
public PrivateKeyAccount(Repository repository, byte[] seed) {
|
public PrivateKeyAccount(Repository repository, byte[] privateKey) {
|
||||||
this(repository, new Ed25519PrivateKeyParameters(seed, 0));
|
this(repository, new Ed25519PrivateKeyParameters(privateKey, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private PrivateKeyAccount(Repository repository, Ed25519PrivateKeyParameters edPrivateKeyParams) {
|
private PrivateKeyAccount(Repository repository, Ed25519PrivateKeyParameters edPrivateKeyParams) {
|
||||||
@ -37,10 +37,6 @@ public class PrivateKeyAccount extends PublicKeyAccount {
|
|||||||
return this.privateKey;
|
return this.privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] toPublicKey(byte[] seed) {
|
|
||||||
return new Ed25519PrivateKeyParameters(seed, 0).generatePublicKey().getEncoded();
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] sign(byte[] message) {
|
public byte[] sign(byte[] message) {
|
||||||
return Crypto.sign(this.edPrivateKeyParams, message);
|
return Crypto.sign(this.edPrivateKeyParams, message);
|
||||||
}
|
}
|
||||||
|
367
src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
Normal file
367
src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
package org.qortal.account;
|
||||||
|
|
||||||
|
import org.qortal.api.resource.TransactionsResource;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.naming.NameData;
|
||||||
|
import org.qortal.data.transaction.*;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class SelfSponsorshipAlgoV1 {
|
||||||
|
|
||||||
|
private final Repository repository;
|
||||||
|
private final String address;
|
||||||
|
private final AccountData accountData;
|
||||||
|
private final long snapshotTimestamp;
|
||||||
|
private final boolean override;
|
||||||
|
|
||||||
|
private int registeredNameCount = 0;
|
||||||
|
private int suspiciousCount = 0;
|
||||||
|
private int suspiciousPercent = 0;
|
||||||
|
private int consolidationCount = 0;
|
||||||
|
private int bulkIssuanceCount = 0;
|
||||||
|
private int recentSponsorshipCount = 0;
|
||||||
|
|
||||||
|
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||||
|
private final Map<String, List<TransactionData>> paymentsByAddress = new HashMap<>();
|
||||||
|
private final Set<String> sponsees = new LinkedHashSet<>();
|
||||||
|
private Set<String> consolidatedAddresses = new LinkedHashSet<>();
|
||||||
|
private final Set<String> zeroTransactionAddreses = new LinkedHashSet<>();
|
||||||
|
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
public SelfSponsorshipAlgoV1(Repository repository, String address, long snapshotTimestamp, boolean override) throws DataException {
|
||||||
|
this.repository = repository;
|
||||||
|
this.address = address;
|
||||||
|
this.accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||||
|
this.snapshotTimestamp = snapshotTimestamp;
|
||||||
|
this.override = override;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return this.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getPenaltyAddresses() {
|
||||||
|
return this.penaltyAddresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void run() throws DataException {
|
||||||
|
if (this.accountData == null) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchSponsorshipRewardShares();
|
||||||
|
if (this.sponsorshipRewardShares.isEmpty()) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.findConsolidatedRewards();
|
||||||
|
this.findBulkIssuance();
|
||||||
|
this.findRegisteredNameCount();
|
||||||
|
this.findRecentSponsorshipCount();
|
||||||
|
|
||||||
|
int score = this.calculateScore();
|
||||||
|
if (score <= 0 && !override) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String newAddress = this.getDestinationAccount(this.address);
|
||||||
|
while (newAddress != null) {
|
||||||
|
// Found destination account
|
||||||
|
this.penaltyAddresses.add(newAddress);
|
||||||
|
|
||||||
|
// Run algo for this address, but in "override" mode because it has already been flagged
|
||||||
|
SelfSponsorshipAlgoV1 algoV1 = new SelfSponsorshipAlgoV1(this.repository, newAddress, this.snapshotTimestamp, true);
|
||||||
|
algoV1.run();
|
||||||
|
this.penaltyAddresses.addAll(algoV1.getPenaltyAddresses());
|
||||||
|
|
||||||
|
newAddress = this.getDestinationAccount(newAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.penaltyAddresses.add(this.address);
|
||||||
|
|
||||||
|
if (this.override || this.recentSponsorshipCount < 20) {
|
||||||
|
this.penaltyAddresses.addAll(this.consolidatedAddresses);
|
||||||
|
this.penaltyAddresses.addAll(this.zeroTransactionAddreses);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.penaltyAddresses.addAll(this.sponsees);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDestinationAccount(String address) throws DataException {
|
||||||
|
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||||
|
if (transferPrivsTransactions.isEmpty()) {
|
||||||
|
// No TRANSFER_PRIVS transactions for this address
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||||
|
if (accountData == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||||
|
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||||
|
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
|
||||||
|
return transferPrivsTransactionData.getRecipient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findConsolidatedRewards() throws DataException {
|
||||||
|
List<String> sponseesThatSentRewards = new ArrayList<>();
|
||||||
|
Map<String, Integer> paymentRecipients = new HashMap<>();
|
||||||
|
|
||||||
|
// Collect outgoing payments of each sponsee
|
||||||
|
for (String sponseeAddress : this.sponsees) {
|
||||||
|
|
||||||
|
// Firstly fetch all payments for address, since the functions below depend on this data
|
||||||
|
this.fetchPaymentsForAddress(sponseeAddress);
|
||||||
|
|
||||||
|
// Check if the address has zero relevant transactions
|
||||||
|
if (this.hasZeroTransactions(sponseeAddress)) {
|
||||||
|
this.zeroTransactionAddreses.add(sponseeAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get payment recipients
|
||||||
|
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||||
|
if (allPaymentRecipients.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sponseesThatSentRewards.add(sponseeAddress);
|
||||||
|
|
||||||
|
List<String> addressesPaidByThisSponsee = new ArrayList<>();
|
||||||
|
for (String paymentRecipient : allPaymentRecipients) {
|
||||||
|
if (addressesPaidByThisSponsee.contains(paymentRecipient)) {
|
||||||
|
// We already tracked this association - don't allow multiple to stack up
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addressesPaidByThisSponsee.add(paymentRecipient);
|
||||||
|
|
||||||
|
// Increment count for this recipient, or initialize to 1 if not present
|
||||||
|
if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) {
|
||||||
|
paymentRecipients.put(paymentRecipient, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude addresses with a low number of payments
|
||||||
|
Map<String, Integer> filteredPaymentRecipients = paymentRecipients.entrySet().stream()
|
||||||
|
.filter(p -> p.getValue() != null && p.getValue() >= 10)
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
// Now check how many sponsees have sent to this subset of addresses
|
||||||
|
Map<String, Integer> sponseesThatConsolidatedRewards = new HashMap<>();
|
||||||
|
for (String sponseeAddress : sponseesThatSentRewards) {
|
||||||
|
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||||
|
// Remove any that aren't to one of the flagged recipients (i.e. consolidation)
|
||||||
|
allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r));
|
||||||
|
|
||||||
|
int count = allPaymentRecipients.size();
|
||||||
|
if (count == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) {
|
||||||
|
sponseesThatConsolidatedRewards.put(sponseeAddress, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sponsees that have only sent a low number of payments to the filtered addresses
|
||||||
|
Map<String, Integer> filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream()
|
||||||
|
.filter(p -> p.getValue() != null && p.getValue() >= 2)
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
this.consolidationCount = sponseesThatConsolidatedRewards.size();
|
||||||
|
this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet());
|
||||||
|
this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size();
|
||||||
|
this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findBulkIssuance() {
|
||||||
|
Long lastTimestamp = null;
|
||||||
|
for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) {
|
||||||
|
long timestamp = rewardShareTransactionData.getTimestamp();
|
||||||
|
if (timestamp >= this.snapshotTimestamp) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTimestamp != null) {
|
||||||
|
if (timestamp - lastTimestamp < 3*60*1000L) {
|
||||||
|
this.bulkIssuanceCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findRegisteredNameCount() throws DataException {
|
||||||
|
int registeredNameCount = 0;
|
||||||
|
for (String sponseeAddress : sponsees) {
|
||||||
|
List<NameData> names = repository.getNameRepository().getNamesByOwner(sponseeAddress);
|
||||||
|
for (NameData name : names) {
|
||||||
|
if (name.getRegistered() < this.snapshotTimestamp) {
|
||||||
|
registeredNameCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.registeredNameCount = registeredNameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findRecentSponsorshipCount() {
|
||||||
|
final long referenceTimestamp = this.snapshotTimestamp - (365 * 24 * 60 * 60 * 1000L);
|
||||||
|
int recentSponsorshipCount = 0;
|
||||||
|
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
|
||||||
|
if (rewardShare.getTimestamp() >= referenceTimestamp) {
|
||||||
|
recentSponsorshipCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.recentSponsorshipCount = recentSponsorshipCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateScore() {
|
||||||
|
final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1;
|
||||||
|
final int nameMultiplier = (this.sponsees.size() >= 50 && this.registeredNameCount == 0) ? 2 : 1;
|
||||||
|
final int consolidationMultiplier = Math.max(this.consolidationCount, 1);
|
||||||
|
final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1);
|
||||||
|
final int offset = 9;
|
||||||
|
return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchSponsorshipRewardShares() throws DataException {
|
||||||
|
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||||
|
|
||||||
|
// Define relevant transactions
|
||||||
|
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
|
||||||
|
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, this.address, false);
|
||||||
|
|
||||||
|
for (TransactionData transactionData : transactionDataList) {
|
||||||
|
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
|
||||||
|
|
||||||
|
// Skip removals
|
||||||
|
if (rewardShareTransactionData.getSharePercent() < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if not sponsored by this account
|
||||||
|
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip self shares
|
||||||
|
if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean duplicateFound = false;
|
||||||
|
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
|
||||||
|
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
|
||||||
|
// Duplicate
|
||||||
|
duplicateFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!duplicateFound) {
|
||||||
|
sponsorshipRewardShares.add(rewardShareTransactionData);
|
||||||
|
this.sponsees.add(rewardShareTransactionData.getRecipient());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sponsorshipRewardShares = sponsorshipRewardShares;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
|
||||||
|
return fetchTransactions(repository,
|
||||||
|
List.of(TransactionType.TRANSFER_PRIVS),
|
||||||
|
address, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchPaymentsForAddress(String address) throws DataException {
|
||||||
|
List<TransactionData> payments = fetchTransactions(repository,
|
||||||
|
Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET),
|
||||||
|
address, false);
|
||||||
|
this.paymentsByAddress.put(address, payments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> fetchOutgoingPaymentRecipientsForAddress(String address) {
|
||||||
|
List<String> outgoingPaymentRecipients = new ArrayList<>();
|
||||||
|
|
||||||
|
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||||
|
if (transactionDataList == null) transactionDataList = new ArrayList<>();
|
||||||
|
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||||
|
for (TransactionData transactionData : transactionDataList) {
|
||||||
|
switch (transactionData.getType()) {
|
||||||
|
|
||||||
|
case PAYMENT:
|
||||||
|
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
|
||||||
|
if (!Objects.equals(paymentTransactionData.getRecipient(), address)) {
|
||||||
|
// Outgoing payment from this account
|
||||||
|
outgoingPaymentRecipients.add(paymentTransactionData.getRecipient());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TRANSFER_ASSET:
|
||||||
|
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
|
||||||
|
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
|
||||||
|
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
|
||||||
|
// Outgoing payment from this account
|
||||||
|
outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outgoingPaymentRecipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasZeroTransactions(String address) {
|
||||||
|
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||||
|
if (transactionDataList == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||||
|
return transactionDataList.size() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||||
|
// Fetch all relevant transactions for this account
|
||||||
|
List<byte[]> signatures = repository.getTransactionRepository()
|
||||||
|
.getSignaturesMatchingCriteria(null, null, null, txTypes,
|
||||||
|
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
|
||||||
|
null, null, reverse);
|
||||||
|
|
||||||
|
List<TransactionData> transactionDataList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (byte[] signature : signatures) {
|
||||||
|
// Fetch transaction data
|
||||||
|
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||||
|
if (transactionData == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
transactionDataList.add(transactionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionDataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
56
src/main/java/org/qortal/api/model/AccountPenaltyStats.java
Normal file
56
src/main/java/org/qortal/api/model/AccountPenaltyStats.java
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package org.qortal.api.model;
|
||||||
|
|
||||||
|
import org.qortal.block.SelfSponsorshipAlgoV1Block;
|
||||||
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.naming.NameData;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class AccountPenaltyStats {
|
||||||
|
|
||||||
|
public Integer totalPenalties;
|
||||||
|
public Integer maxPenalty;
|
||||||
|
public Integer minPenalty;
|
||||||
|
public String penaltyHash;
|
||||||
|
|
||||||
|
protected AccountPenaltyStats() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountPenaltyStats(Integer totalPenalties, Integer maxPenalty, Integer minPenalty, String penaltyHash) {
|
||||||
|
this.totalPenalties = totalPenalties;
|
||||||
|
this.maxPenalty = maxPenalty;
|
||||||
|
this.minPenalty = minPenalty;
|
||||||
|
this.penaltyHash = penaltyHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountPenaltyStats fromAccounts(List<AccountData> accounts) {
|
||||||
|
int totalPenalties = 0;
|
||||||
|
Integer maxPenalty = null;
|
||||||
|
Integer minPenalty = null;
|
||||||
|
|
||||||
|
List<String> addresses = new ArrayList<>();
|
||||||
|
for (AccountData accountData : accounts) {
|
||||||
|
int penalty = accountData.getBlocksMintedPenalty();
|
||||||
|
addresses.add(accountData.getAddress());
|
||||||
|
totalPenalties++;
|
||||||
|
|
||||||
|
// Penalties are expressed as a negative number, so the min and the max are reversed here
|
||||||
|
if (maxPenalty == null || penalty < maxPenalty) maxPenalty = penalty;
|
||||||
|
if (minPenalty == null || penalty > minPenalty) minPenalty = penalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
String penaltyHash = SelfSponsorshipAlgoV1Block.getHash(addresses);
|
||||||
|
return new AccountPenaltyStats(totalPenalties, maxPenalty, minPenalty, penaltyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("totalPenalties: %d, maxPenalty: %d, minPenalty: %d, penaltyHash: %s", totalPenalties, maxPenalty, minPenalty, penaltyHash == null ? "null" : penaltyHash);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
package org.qortal.api.model;
|
package org.qortal.api.model;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import org.qortal.data.network.PeerChainTipData;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.data.block.BlockSummaryData;
|
||||||
import org.qortal.data.network.PeerData;
|
import org.qortal.data.network.PeerData;
|
||||||
import org.qortal.network.Handshake;
|
import org.qortal.network.Handshake;
|
||||||
import org.qortal.network.Peer;
|
import org.qortal.network.Peer;
|
||||||
@ -36,6 +37,7 @@ public class ConnectedPeer {
|
|||||||
public Long lastBlockTimestamp;
|
public Long lastBlockTimestamp;
|
||||||
public UUID connectionId;
|
public UUID connectionId;
|
||||||
public String age;
|
public String age;
|
||||||
|
public Boolean isTooDivergent;
|
||||||
|
|
||||||
protected ConnectedPeer() {
|
protected ConnectedPeer() {
|
||||||
}
|
}
|
||||||
@ -63,11 +65,16 @@ public class ConnectedPeer {
|
|||||||
this.age = "connecting...";
|
this.age = "connecting...";
|
||||||
}
|
}
|
||||||
|
|
||||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
if (peerChainTipData != null) {
|
if (peerChainTipData != null) {
|
||||||
this.lastHeight = peerChainTipData.getLastHeight();
|
this.lastHeight = peerChainTipData.getHeight();
|
||||||
this.lastBlockSignature = peerChainTipData.getLastBlockSignature();
|
this.lastBlockSignature = peerChainTipData.getSignature();
|
||||||
this.lastBlockTimestamp = peerChainTipData.getLastBlockTimestamp();
|
this.lastBlockTimestamp = peerChainTipData.getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include isTooDivergent decision if we've had the opportunity to request block summaries this peer
|
||||||
|
if (peer.getLastTooDivergentTime() != null) {
|
||||||
|
this.isTooDivergent = Controller.wasRecentlyTooDivergent.test(peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import javax.xml.bind.annotation.XmlAccessType;
|
|||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.OnlineAccountsManager;
|
||||||
import org.qortal.controller.Synchronizer;
|
import org.qortal.controller.Synchronizer;
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ public class NodeStatus {
|
|||||||
public final int height;
|
public final int height;
|
||||||
|
|
||||||
public NodeStatus() {
|
public NodeStatus() {
|
||||||
this.isMintingPossible = Controller.getInstance().isMintingPossible();
|
this.isMintingPossible = OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures();
|
||||||
|
|
||||||
this.syncPercent = Synchronizer.getInstance().getSyncPercent();
|
this.syncPercent = Synchronizer.getInstance().getSyncPercent();
|
||||||
this.isSynchronizing = Synchronizer.getInstance().isSynchronizing();
|
this.isSynchronizing = Synchronizer.getInstance().isSynchronizing();
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.qortal.api.model.crosschain;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class PirateChainSendRequest {
|
||||||
|
|
||||||
|
@Schema(description = "32 bytes of entropy, Base58 encoded", example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV")
|
||||||
|
public String entropy58;
|
||||||
|
|
||||||
|
@Schema(description = "Recipient's Pirate Chain address", example = "zc...")
|
||||||
|
public String receivingAddress;
|
||||||
|
|
||||||
|
@Schema(description = "Amount of ARRR to send", type = "number")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public long arrrAmount;
|
||||||
|
|
||||||
|
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 ARRR (100 sats) per byte", example = "0.00000100", type = "number")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public Long feePerByte;
|
||||||
|
|
||||||
|
@Schema(description = "Optional memo to include information for the recipient", example = "zc...")
|
||||||
|
public String memo;
|
||||||
|
|
||||||
|
public PirateChainSendRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -14,6 +14,7 @@ import java.math.BigDecimal;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.*;
|
||||||
@ -27,6 +28,7 @@ import org.qortal.api.ApiErrors;
|
|||||||
import org.qortal.api.ApiException;
|
import org.qortal.api.ApiException;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
|
import org.qortal.api.model.AccountPenaltyStats;
|
||||||
import org.qortal.api.model.ApiOnlineAccount;
|
import org.qortal.api.model.ApiOnlineAccount;
|
||||||
import org.qortal.api.model.RewardShareKeyRequest;
|
import org.qortal.api.model.RewardShareKeyRequest;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
@ -34,6 +36,7 @@ import org.qortal.controller.LiteNode;
|
|||||||
import org.qortal.controller.OnlineAccountsManager;
|
import org.qortal.controller.OnlineAccountsManager;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.account.AccountPenaltyData;
|
||||||
import org.qortal.data.account.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
import org.qortal.data.network.OnlineAccountData;
|
import org.qortal.data.network.OnlineAccountData;
|
||||||
import org.qortal.data.network.OnlineAccountLevel;
|
import org.qortal.data.network.OnlineAccountLevel;
|
||||||
@ -205,6 +208,10 @@ public class AddressesResource {
|
|||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
List<OnlineAccountLevel> onlineAccountLevels = new ArrayList<>();
|
List<OnlineAccountLevel> onlineAccountLevels = new ArrayList<>();
|
||||||
|
|
||||||
|
// Prepopulate all levels
|
||||||
|
for (int i=0; i<=10; i++)
|
||||||
|
onlineAccountLevels.add(new OnlineAccountLevel(i, 0));
|
||||||
|
|
||||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||||
try {
|
try {
|
||||||
final int minterLevel = Account.getRewardShareEffectiveMintingLevelIncludingLevelZero(repository, onlineAccountData.getPublicKey());
|
final int minterLevel = Account.getRewardShareEffectiveMintingLevelIncludingLevelZero(repository, onlineAccountData.getPublicKey());
|
||||||
@ -467,6 +474,54 @@ public class AddressesResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/penalties")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get addresses with penalties",
|
||||||
|
description = "Returns a list of accounts with a blocksMintedPenalty",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "accounts with penalties",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AccountPenaltyData.class)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public List<AccountPenaltyData> getAccountsWithPenalties() {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||||
|
List<AccountPenaltyData> penalties = accounts.stream().map(a -> new AccountPenaltyData(a.getAddress(), a.getBlocksMintedPenalty())).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return penalties;
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/penalties/stats")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get stats about current penalties",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "aggregated stats about accounts with penalties",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AccountPenaltyStats.class)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public AccountPenaltyStats getPenaltyStats() {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||||
|
return AccountPenaltyStats.fromAccounts(accounts);
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/publicize")
|
@Path("/publicize")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -125,12 +125,12 @@ public class AdminResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getNodeType() {
|
private String getNodeType() {
|
||||||
if (Settings.getInstance().isTopOnly()) {
|
if (Settings.getInstance().isLite()) {
|
||||||
return "topOnly";
|
|
||||||
}
|
|
||||||
else if (Settings.getInstance().isLite()) {
|
|
||||||
return "lite";
|
return "lite";
|
||||||
}
|
}
|
||||||
|
else if (Settings.getInstance().isTopOnly()) {
|
||||||
|
return "topOnly";
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
return "full";
|
return "full";
|
||||||
}
|
}
|
||||||
@ -728,6 +728,49 @@ public class AdminResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/repository/importarchivedtrades")
|
||||||
|
@Operation(
|
||||||
|
summary = "Imports archived trades from TradeBotStatesArchive.json",
|
||||||
|
description = "This can be used to recover trades that exist in the archive only, which may be needed if a<br />" +
|
||||||
|
"problem occurred during the proof-of-work computation stage of a buy request.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public boolean importArchivedTrades(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
|
|
||||||
|
blockchainLock.lockInterruptibly();
|
||||||
|
|
||||||
|
try {
|
||||||
|
repository.importDataFromFile("qortal-backup/TradeBotStatesArchive.json");
|
||||||
|
repository.saveChanges();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
blockchainLock.unlock();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// We couldn't lock blockchain to perform import
|
||||||
|
return false;
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/apikey/generate")
|
@Path("/apikey/generate")
|
||||||
|
@ -12,10 +12,10 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
@ -45,6 +45,7 @@ import org.qortal.data.arbitrary.*;
|
|||||||
import org.qortal.data.naming.NameData;
|
import org.qortal.data.naming.NameData;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.list.ResourceListManager;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
@ -56,6 +57,7 @@ import org.qortal.transaction.Transaction.ValidationResult;
|
|||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
||||||
import org.qortal.transform.transaction.TransactionTransformer;
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
|
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
import org.qortal.utils.ZipUtils;
|
import org.qortal.utils.ZipUtils;
|
||||||
@ -91,6 +93,7 @@ public class ArbitraryResource {
|
|||||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
|
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
|
||||||
|
@Parameter(description = "Filter names by list") @QueryParam("namefilter") String nameFilter,
|
||||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
|
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
|
||||||
|
|
||||||
@ -107,8 +110,18 @@ public class ArbitraryResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "identifier cannot be specified when requesting a default resource");
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "identifier cannot be specified when requesting a default resource");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load filter from list if needed
|
||||||
|
List<String> names = null;
|
||||||
|
if (nameFilter != null) {
|
||||||
|
names = ResourceListManager.getInstance().getStringsInList(nameFilter);
|
||||||
|
if (names.isEmpty()) {
|
||||||
|
// List doesn't exist or is empty - so there will be no matches
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||||
.getArbitraryResources(service, identifier, null, defaultRes, limit, offset, reverse);
|
.getArbitraryResources(service, identifier, names, defaultRes, limit, offset, reverse);
|
||||||
|
|
||||||
if (resources == null) {
|
if (resources == null) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
@ -216,7 +229,7 @@ public class ArbitraryResource {
|
|||||||
String name = creatorName.name;
|
String name = creatorName.name;
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||||
.getArbitraryResources(service, identifier, name, defaultRes, null, null, reverse);
|
.getArbitraryResources(service, identifier, Arrays.asList(name), defaultRes, null, null, reverse);
|
||||||
|
|
||||||
if (includeStatus != null && includeStatus) {
|
if (includeStatus != null && includeStatus) {
|
||||||
resources = this.addStatusToResources(resources);
|
resources = this.addStatusToResources(resources);
|
||||||
@ -254,7 +267,7 @@ public class ArbitraryResource {
|
|||||||
@QueryParam("build") Boolean build) {
|
@QueryParam("build") Boolean build) {
|
||||||
|
|
||||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
|
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
|
||||||
return this.getStatus(service, name, null, build);
|
return ArbitraryTransactionUtils.getStatus(service, name, null, build);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@ -276,7 +289,7 @@ public class ArbitraryResource {
|
|||||||
@QueryParam("build") Boolean build) {
|
@QueryParam("build") Boolean build) {
|
||||||
|
|
||||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
|
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
|
||||||
return this.getStatus(service, name, identifier, build);
|
return ArbitraryTransactionUtils.getStatus(service, name, identifier, build);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -706,7 +719,7 @@ public class ArbitraryResource {
|
|||||||
try {
|
try {
|
||||||
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false);
|
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false);
|
||||||
if (transactionMetadata != null) {
|
if (transactionMetadata != null) {
|
||||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, true);
|
||||||
if (resourceMetadata != null) {
|
if (resourceMetadata != null) {
|
||||||
return resourceMetadata;
|
return resourceMetadata;
|
||||||
}
|
}
|
||||||
@ -1115,7 +1128,7 @@ public class ArbitraryResource {
|
|||||||
if (path == null) {
|
if (path == null) {
|
||||||
// See if we have a string instead
|
// See if we have a string instead
|
||||||
if (string != null) {
|
if (string != null) {
|
||||||
File tempFile = File.createTempFile("qortal-", ".tmp");
|
File tempFile = File.createTempFile("qortal-", "");
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString()));
|
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString()));
|
||||||
writer.write(string);
|
writer.write(string);
|
||||||
@ -1125,7 +1138,7 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
// ... or base64 encoded raw data
|
// ... or base64 encoded raw data
|
||||||
else if (base64 != null) {
|
else if (base64 != null) {
|
||||||
File tempFile = File.createTempFile("qortal-", ".tmp");
|
File tempFile = File.createTempFile("qortal-", "");
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
Files.write(tempFile.toPath(), Base64.decode(base64));
|
Files.write(tempFile.toPath(), Base64.decode(base64));
|
||||||
path = tempFile.toPath().toString();
|
path = tempFile.toPath().toString();
|
||||||
@ -1247,24 +1260,6 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
|
|
||||||
|
|
||||||
// If "build=true" has been specified in the query string, build the resource before returning its status
|
|
||||||
if (build != null && build == true) {
|
|
||||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
|
||||||
try {
|
|
||||||
if (!reader.isBuilding()) {
|
|
||||||
reader.loadSynchronously(false);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// No need to handle exception, as it will be reflected in the status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
|
||||||
return resource.getStatus(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||||
// Determine and add the status of each resource
|
// Determine and add the status of each resource
|
||||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||||
@ -1293,7 +1288,7 @@ public class ArbitraryResource {
|
|||||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||||
resourceInfo.service, resourceInfo.identifier);
|
resourceInfo.service, resourceInfo.identifier);
|
||||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, false);
|
||||||
if (resourceMetadata != null) {
|
if (resourceMetadata != null) {
|
||||||
resourceInfo.metadata = resourceMetadata;
|
resourceInfo.metadata = resourceMetadata;
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ public class BlocksResource {
|
|||||||
@Path("/signature/{signature}/data")
|
@Path("/signature/{signature}/data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Fetch serialized, base58 encoded block data using base58 signature",
|
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 = {
|
responses = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
description = "the block data",
|
description = "the block data",
|
||||||
@ -125,7 +125,7 @@ public class BlocksResource {
|
|||||||
@ApiErrors({
|
@ApiErrors({
|
||||||
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
|
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
|
// Decode signature
|
||||||
byte[] signature;
|
byte[] signature;
|
||||||
try {
|
try {
|
||||||
@ -136,20 +136,41 @@ public class BlocksResource {
|
|||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Default to version 1
|
||||||
|
if (version == null) {
|
||||||
|
version = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Check the database first
|
// Check the database first
|
||||||
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
|
||||||
if (blockData != null) {
|
if (blockData != null) {
|
||||||
Block block = new Block(repository, blockData);
|
Block block = new Block(repository, blockData);
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
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());
|
return Base58.encode(bytes.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not found, so try the block archive
|
// Not found, so try the block archive
|
||||||
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
|
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
|
||||||
if (bytes != null) {
|
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);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
|
||||||
@ -613,13 +634,16 @@ public class BlocksResource {
|
|||||||
@ApiErrors({
|
@ApiErrors({
|
||||||
ApiError.REPOSITORY_ISSUE
|
ApiError.REPOSITORY_ISSUE
|
||||||
})
|
})
|
||||||
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
|
public List<BlockData> getBlockRange(@PathParam("height") int height,
|
||||||
ref = "count"
|
@Parameter(ref = "count") @QueryParam("count") int count,
|
||||||
) @QueryParam("count") int count) {
|
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
|
||||||
|
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
List<BlockData> blocks = new ArrayList<>();
|
List<BlockData> blocks = new ArrayList<>();
|
||||||
|
boolean shouldReverse = (reverse != null && reverse == true);
|
||||||
|
|
||||||
for (/* count already set */; count > 0; --count, ++height) {
|
int i = 0;
|
||||||
|
while (i < count) {
|
||||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||||
if (blockData == null) {
|
if (blockData == null) {
|
||||||
// Not found - try the archive
|
// Not found - try the archive
|
||||||
@ -629,8 +653,14 @@ public class BlocksResource {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||||
|
blockData.setOnlineAccountsSignatures(null);
|
||||||
|
}
|
||||||
|
|
||||||
blocks.add(blockData);
|
blocks.add(blockData);
|
||||||
|
|
||||||
|
height = shouldReverse ? height - 1 : height + 1;
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks;
|
return blocks;
|
||||||
|
@ -69,6 +69,9 @@ public class ChatResource {
|
|||||||
public List<ChatMessage> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
|
public List<ChatMessage> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
|
||||||
@QueryParam("txGroupId") Integer txGroupId,
|
@QueryParam("txGroupId") Integer txGroupId,
|
||||||
@QueryParam("involving") List<String> involvingAddresses,
|
@QueryParam("involving") List<String> involvingAddresses,
|
||||||
|
@QueryParam("reference") String reference,
|
||||||
|
@QueryParam("chatreference") String chatReference,
|
||||||
|
@QueryParam("haschatreference") Boolean hasChatReference,
|
||||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||||
@ -87,11 +90,22 @@ public class ChatResource {
|
|||||||
if (after != null && after < 1500000000000L)
|
if (after != null && after < 1500000000000L)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
byte[] referenceBytes = null;
|
||||||
|
if (reference != null)
|
||||||
|
referenceBytes = Base58.decode(reference);
|
||||||
|
|
||||||
|
byte[] chatReferenceBytes = null;
|
||||||
|
if (chatReference != null)
|
||||||
|
chatReferenceBytes = Base58.decode(chatReference);
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
return repository.getChatRepository().getMessagesMatchingCriteria(
|
return repository.getChatRepository().getMessagesMatchingCriteria(
|
||||||
before,
|
before,
|
||||||
after,
|
after,
|
||||||
txGroupId,
|
txGroupId,
|
||||||
|
referenceBytes,
|
||||||
|
chatReferenceBytes,
|
||||||
|
hasChatReference,
|
||||||
involvingAddresses,
|
involvingAddresses,
|
||||||
limit, offset, reverse);
|
limit, offset, reverse);
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
@ -99,6 +113,38 @@ public class ChatResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/message/{signature}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Find chat message by signature",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "CHAT message",
|
||||||
|
content = @Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = ChatMessage.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public ChatMessage getMessageBySignature(@PathParam("signature") String signature58) {
|
||||||
|
byte[] signature = Base58.decode(signature58);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
ChatTransactionData chatTransactionData = (ChatTransactionData) repository.getTransactionRepository().fromSignature(signature);
|
||||||
|
if (chatTransactionData == null) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Message not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return repository.getChatRepository().toChatMessage(chatTransactionData);
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/active/{address}")
|
@Path("/active/{address}")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -68,7 +68,7 @@ public class CrossChainBitcoinResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = bitcoin.getWalletBalanceFromTransactions(key58);
|
Long balance = bitcoin.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ public class CrossChainDigibyteResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = digibyte.getWalletBalanceFromTransactions(key58);
|
Long balance = digibyte.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ public class CrossChainDogecoinResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = dogecoin.getWalletBalanceFromTransactions(key58);
|
Long balance = dogecoin.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.qortal.api.resource;
|
package org.qortal.api.resource;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@ -9,6 +10,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.*;
|
||||||
@ -284,6 +287,12 @@ public class CrossChainHtlcResource {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
|
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||||
|
LOGGER.info("Skipping AT {} because ARRR is currently unsupported", atAddress);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
||||||
if (crossChainTradeData == null) {
|
if (crossChainTradeData == null) {
|
||||||
LOGGER.info("Couldn't find crosschain trade data for AT {}", atAddress);
|
LOGGER.info("Couldn't find crosschain trade data for AT {}", atAddress);
|
||||||
@ -363,10 +372,6 @@ public class CrossChainHtlcResource {
|
|||||||
// Use secret-A to redeem P2SH-A
|
// Use secret-A to redeem P2SH-A
|
||||||
|
|
||||||
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
if (bitcoiny.getClass() == Bitcoin.class) {
|
|
||||||
LOGGER.info("Redeeming a Bitcoin HTLC is not yet supported");
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
}
|
|
||||||
|
|
||||||
int lockTime = crossChainTradeData.lockTimeA;
|
int lockTime = crossChainTradeData.lockTimeA;
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
@ -574,70 +579,108 @@ public class CrossChainHtlcResource {
|
|||||||
// If the AT is "finished" then it will have a zero balance
|
// If the AT is "finished" then it will have a zero balance
|
||||||
// In these cases we should avoid HTLC refunds if tbe QORT haven't been returned to the seller
|
// In these cases we should avoid HTLC refunds if tbe QORT haven't been returned to the seller
|
||||||
if (atData.getIsFinished() && crossChainTradeData.mode != AcctMode.REFUNDED && crossChainTradeData.mode != AcctMode.CANCELLED) {
|
if (atData.getIsFinished() && crossChainTradeData.mode != AcctMode.REFUNDED && crossChainTradeData.mode != AcctMode.CANCELLED) {
|
||||||
LOGGER.info(String.format("Skipping AT %s because the QORT has already been redemed", atAddress));
|
LOGGER.info(String.format("Skipping AT %s because the QORT has already been redeemed by the buyer", atAddress));
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
||||||
TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
|
List<TradeBotData> tradeBotDataList = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).collect(Collectors.toList());
|
||||||
if (tradeBotData == null)
|
if (tradeBotDataList == null || tradeBotDataList.isEmpty())
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
// Loop through all matching entries for this AT address, as there might be more than one
|
||||||
if (bitcoiny.getClass() == Bitcoin.class) {
|
for (TradeBotData tradeBotData : tradeBotDataList) {
|
||||||
LOGGER.info("Refunding a Bitcoin HTLC is not yet supported");
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
}
|
|
||||||
|
|
||||||
int lockTime = tradeBotData.getLockTimeA();
|
if (tradeBotData == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
// We can't refund P2SH-A until lockTime-A has passed
|
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||||
if (NTP.getTime() <= lockTime * 1000L)
|
int lockTime = tradeBotData.getLockTimeA();
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
|
||||||
|
|
||||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
// We can't refund P2SH-A until lockTime-A has passed
|
||||||
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
if (NTP.getTime() <= lockTime * 1000L)
|
||||||
if (medianBlockTime <= lockTime)
|
continue;
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
|
||||||
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||||
String p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||||
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
if (medianBlockTime <= lockTime)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
// Create redeem script based on destination chain
|
||||||
case UNFUNDED:
|
byte[] redeemScriptA;
|
||||||
case FUNDING_IN_PROGRESS:
|
String p2shAddressA;
|
||||||
// Still waiting for P2SH-A to be funded...
|
BitcoinyHTLC.Status htlcStatusA;
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||||
|
redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
|
p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA);
|
||||||
|
htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
} else {
|
||||||
|
redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
|
p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||||
|
htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
}
|
||||||
|
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
||||||
|
|
||||||
case REDEEM_IN_PROGRESS:
|
switch (htlcStatusA) {
|
||||||
case REDEEMED:
|
case UNFUNDED:
|
||||||
case REFUND_IN_PROGRESS:
|
case FUNDING_IN_PROGRESS:
|
||||||
case REFUNDED:
|
// Still waiting for P2SH-A to be funded...
|
||||||
// Too late!
|
continue;
|
||||||
return false;
|
|
||||||
|
|
||||||
case FUNDED:{
|
case REDEEM_IN_PROGRESS:
|
||||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
case REDEEMED:
|
||||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
case REFUND_IN_PROGRESS:
|
||||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
case REFUNDED:
|
||||||
|
// Too late!
|
||||||
|
continue;
|
||||||
|
|
||||||
// Validate the destination foreign blockchain address
|
case FUNDED: {
|
||||||
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
|
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
|
||||||
|
|
||||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||||
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
// Pirate Chain custom integration
|
||||||
|
|
||||||
bitcoiny.broadcastTransaction(p2shRefundTransaction);
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
return true;
|
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA);
|
||||||
|
|
||||||
|
// Get funding txid
|
||||||
|
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
|
if (fundingTxidHex == null) {
|
||||||
|
throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
|
||||||
|
}
|
||||||
|
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
|
||||||
|
|
||||||
|
byte[] privateKey = tradeBotData.getTradePrivateKey();
|
||||||
|
String privateKey58 = Base58.encode(privateKey);
|
||||||
|
String redeemScript58 = Base58.encode(redeemScriptA);
|
||||||
|
|
||||||
|
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
|
||||||
|
receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58);
|
||||||
|
LOGGER.info("Refund txid: {}", txid);
|
||||||
|
} else {
|
||||||
|
// ElectrumX coins
|
||||||
|
|
||||||
|
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
|
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||||
|
|
||||||
|
// Validate the destination foreign blockchain address
|
||||||
|
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
|
||||||
|
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
||||||
|
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
||||||
|
|
||||||
|
bitcoiny.broadcastTransaction(p2shRefundTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ public class CrossChainLitecoinResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = litecoin.getWalletBalanceFromTransactions(key58);
|
Long balance = litecoin.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -0,0 +1,229 @@
|
|||||||
|
package org.qortal.api.resource;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.qortal.api.ApiError;
|
||||||
|
import org.qortal.api.ApiErrors;
|
||||||
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
|
import org.qortal.api.Security;
|
||||||
|
import org.qortal.api.model.crosschain.PirateChainSendRequest;
|
||||||
|
import org.qortal.crosschain.ForeignBlockchainException;
|
||||||
|
import org.qortal.crosschain.PirateChain;
|
||||||
|
import org.qortal.crosschain.SimpleTransaction;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.ws.rs.HeaderParam;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Path("/crosschain/arrr")
|
||||||
|
@Tag(name = "Cross-Chain (Pirate Chain)")
|
||||||
|
public class CrossChainPirateChainResource {
|
||||||
|
|
||||||
|
@Context
|
||||||
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/walletbalance")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns ARRR balance",
|
||||||
|
description = "Supply 32 bytes of entropy, Base58 encoded",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "32 bytes of entropy, Base58 encoded",
|
||||||
|
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public String getPirateChainWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Long balance = pirateChain.getWalletBalance(entropy58);
|
||||||
|
if (balance == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
return balance.toString();
|
||||||
|
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/wallettransactions")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns transactions",
|
||||||
|
description = "Supply 32 bytes of entropy, Base58 encoded",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "32 bytes of entropy, Base58 encoded",
|
||||||
|
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public List<SimpleTransaction> getPirateChainWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return pirateChain.getWalletTransactions(entropy58);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/send")
|
||||||
|
@Operation(
|
||||||
|
summary = "Sends ARRR from wallet",
|
||||||
|
description = "Currently supports 'legacy' P2PKH PirateChain addresses and Native SegWit (P2WPKH) addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "32 bytes of entropy, Base58 encoded",
|
||||||
|
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public String sendBitcoin(@HeaderParam(Security.API_KEY_HEADER) String apiKey, PirateChainSendRequest pirateChainSendRequest) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
if (pirateChainSendRequest.arrrAmount <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
if (pirateChainSendRequest.feePerByte != null && pirateChainSendRequest.feePerByte <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return pirateChain.sendCoins(pirateChainSendRequest);
|
||||||
|
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
// TODO
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/walletaddress")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns main wallet address",
|
||||||
|
description = "Supply 32 bytes of entropy, Base58 encoded",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "32 bytes of entropy, Base58 encoded",
|
||||||
|
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public String getPirateChainWalletAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return pirateChain.getWalletAddress(entropy58);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/syncstatus")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns synchronization status",
|
||||||
|
description = "Supply 32 bytes of entropy, Base58 encoded",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "32 bytes of entropy, Base58 encoded",
|
||||||
|
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public String getPirateChainSyncStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return pirateChain.getSyncStatus(entropy58);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,7 @@ public class CrossChainRavencoinResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = ravencoin.getWalletBalanceFromTransactions(key58);
|
Long balance = ravencoin.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@ -38,9 +39,12 @@ import org.qortal.crypto.Crypto;
|
|||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.data.crosschain.TradeBotData;
|
import org.qortal.data.crosschain.TradeBotData;
|
||||||
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@ -155,7 +159,7 @@ public class CrossChainTradeBotResource {
|
|||||||
|
|
||||||
return Base58.encode(unsignedBytes);
|
return Base58.encode(unsignedBytes);
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +227,17 @@ public class CrossChainTradeBotResource {
|
|||||||
if (crossChainTradeData.mode != AcctMode.OFFERING)
|
if (crossChainTradeData.mode != AcctMode.OFFERING)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
// Check if there is a buy or a cancel request in progress for this trade
|
||||||
|
List<Transaction.TransactionType> txTypes = List.of(Transaction.TransactionType.MESSAGE);
|
||||||
|
List<TransactionData> unconfirmed = repository.getTransactionRepository().getUnconfirmedTransactions(txTypes, null, 0, 0, false);
|
||||||
|
for (TransactionData transactionData : unconfirmed) {
|
||||||
|
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
|
||||||
|
if (Objects.equals(messageTransactionData.getRecipient(), atAddress)) {
|
||||||
|
// There is a pending request for this trade, so block this buy attempt to reduce the risk of refunds
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Trade has an existing buy request or is pending cancellation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AcctTradeBot.ResponseResult result = TradeBot.getInstance().startResponse(repository, atData, acct, crossChainTradeData,
|
AcctTradeBot.ResponseResult result = TradeBot.getInstance().startResponse(repository, atData, acct, crossChainTradeData,
|
||||||
tradeBotRespondRequest.foreignKey, tradeBotRespondRequest.receivingAddress);
|
tradeBotRespondRequest.foreignKey, tradeBotRespondRequest.receivingAddress);
|
||||||
|
|
||||||
@ -240,7 +255,7 @@ public class CrossChainTradeBotResource {
|
|||||||
return "false";
|
return "false";
|
||||||
}
|
}
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -748,7 +748,7 @@ public class TransactionsResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
|
||||||
|
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS))
|
if (!blockchainLock.tryLock(60, TimeUnit.SECONDS))
|
||||||
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
|
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -46,6 +46,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
|||||||
null,
|
null,
|
||||||
txGroupId,
|
txGroupId,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
null, null, null);
|
null, null, null);
|
||||||
|
|
||||||
sendMessages(session, chatMessages);
|
sendMessages(session, chatMessages);
|
||||||
@ -69,6 +72,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
|||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
|
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
@ -2,10 +2,7 @@ package org.qortal.api.websocket;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.util.Collections;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.eclipse.jetty.websocket.api.Session;
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
@ -85,6 +82,7 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
|
|||||||
@Override
|
@Override
|
||||||
public void onWebSocketConnect(Session session) {
|
public void onWebSocketConnect(Session session) {
|
||||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||||
|
final boolean excludeInitialData = queryParams.get("excludeInitialData") != null;
|
||||||
|
|
||||||
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
|
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
|
||||||
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
|
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
|
||||||
@ -98,15 +96,22 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
|
|||||||
// save session's preferred blockchain (if any)
|
// save session's preferred blockchain (if any)
|
||||||
sessionBlockchain.put(session, foreignBlockchain);
|
sessionBlockchain.put(session, foreignBlockchain);
|
||||||
|
|
||||||
// Send all known trade-bot entries
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
|
|
||||||
|
|
||||||
// Optional filtering
|
|
||||||
if (foreignBlockchain != null)
|
// Maybe send all known trade-bot entries
|
||||||
tradeBotEntries = tradeBotEntries.stream()
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
.filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain))
|
List<TradeBotData> tradeBotEntries = new ArrayList<>();
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
// We might need to exclude the initial data from the response
|
||||||
|
if (!excludeInitialData) {
|
||||||
|
tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
|
||||||
|
|
||||||
|
// Optional filtering
|
||||||
|
if (foreignBlockchain != null)
|
||||||
|
tradeBotEntries = tradeBotEntries.stream()
|
||||||
|
.filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
if (!sendEntries(session, tradeBotEntries)) {
|
if (!sendEntries(session, tradeBotEntries)) {
|
||||||
session.close(4002, "websocket issue");
|
session.close(4002, "websocket issue");
|
||||||
|
@ -173,6 +173,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
|||||||
public void onWebSocketConnect(Session session) {
|
public void onWebSocketConnect(Session session) {
|
||||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||||
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
|
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
|
||||||
|
final boolean excludeInitialData = queryParams.get("excludeInitialData") != null;
|
||||||
|
|
||||||
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
|
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
|
||||||
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
|
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
|
||||||
@ -189,20 +190,23 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
|||||||
|
|
||||||
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
|
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
|
||||||
|
|
||||||
synchronized (cachedInfoByBlockchain) {
|
// We might need to exclude the initial data from the response
|
||||||
Collection<CachedOfferInfo> cachedInfos;
|
if (!excludeInitialData) {
|
||||||
|
synchronized (cachedInfoByBlockchain) {
|
||||||
|
Collection<CachedOfferInfo> cachedInfos;
|
||||||
|
|
||||||
if (foreignBlockchain == null)
|
if (foreignBlockchain == null)
|
||||||
// No preferred blockchain, so iterate through all of them
|
// No preferred blockchain, so iterate through all of them
|
||||||
cachedInfos = cachedInfoByBlockchain.values();
|
cachedInfos = cachedInfoByBlockchain.values();
|
||||||
else
|
else
|
||||||
cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
|
cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
|
||||||
|
|
||||||
for (CachedOfferInfo cachedInfo : cachedInfos) {
|
for (CachedOfferInfo cachedInfo : cachedInfos) {
|
||||||
crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
|
crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
|
||||||
|
|
||||||
if (includeHistoric)
|
if (includeHistoric)
|
||||||
crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
|
crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,11 +65,15 @@ public class TradePresenceWebSocket extends ApiWebSocket implements Listener {
|
|||||||
@Override
|
@Override
|
||||||
public void onWebSocketConnect(Session session) {
|
public void onWebSocketConnect(Session session) {
|
||||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||||
|
final boolean excludeInitialData = queryParams.get("excludeInitialData") != null;
|
||||||
|
|
||||||
List<TradePresenceData> tradePresences;
|
List<TradePresenceData> tradePresences = new ArrayList<>();
|
||||||
|
|
||||||
synchronized (currentEntries) {
|
// We might need to exclude the initial data from the response
|
||||||
tradePresences = List.copyOf(currentEntries.values());
|
if (!excludeInitialData) {
|
||||||
|
synchronized (currentEntries) {
|
||||||
|
tradePresences = List.copyOf(currentEntries.values());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sendTradePresences(session, tradePresences)) {
|
if (!sendTradePresences(session, tradePresences)) {
|
||||||
|
@ -2,6 +2,7 @@ package org.qortal.arbitrary;
|
|||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||||
import org.qortal.arbitrary.exception.MissingDataException;
|
import org.qortal.arbitrary.exception.MissingDataException;
|
||||||
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
|
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
|
||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
@ -88,7 +89,7 @@ public class ArbitraryDataBuilder {
|
|||||||
if (latestPut == null) {
|
if (latestPut == null) {
|
||||||
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
||||||
this.name, this.service, this.identifierString());
|
this.name, this.service, this.identifierString());
|
||||||
throw new DataException(message);
|
throw new DataNotPublishedException(message);
|
||||||
}
|
}
|
||||||
this.latestPutTransaction = latestPut;
|
this.latestPutTransaction = latestPut;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import org.apache.commons.io.FileUtils;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||||
import org.qortal.arbitrary.exception.MissingDataException;
|
import org.qortal.arbitrary.exception.MissingDataException;
|
||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||||
@ -59,6 +60,9 @@ public class ArbitraryDataReader {
|
|||||||
private int layerCount;
|
private int layerCount;
|
||||||
private byte[] latestSignature;
|
private byte[] latestSignature;
|
||||||
|
|
||||||
|
// The resource being read
|
||||||
|
ArbitraryDataResource arbitraryDataResource = null;
|
||||||
|
|
||||||
public ArbitraryDataReader(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
|
public ArbitraryDataReader(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
|
||||||
// Ensure names are always lowercase
|
// Ensure names are always lowercase
|
||||||
if (resourceIdType == ResourceIdType.NAME) {
|
if (resourceIdType == ResourceIdType.NAME) {
|
||||||
@ -115,6 +119,11 @@ public class ArbitraryDataReader {
|
|||||||
return new ArbitraryDataBuildQueueItem(this.resourceId, this.resourceIdType, this.service, this.identifier);
|
return new ArbitraryDataBuildQueueItem(this.resourceId, this.resourceIdType, this.service, this.identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ArbitraryDataResource createArbitraryDataResource() {
|
||||||
|
return new ArbitraryDataResource(this.resourceId, this.resourceIdType, this.service, this.identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* loadAsynchronously
|
* loadAsynchronously
|
||||||
*
|
*
|
||||||
@ -162,6 +171,8 @@ public class ArbitraryDataReader {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.arbitraryDataResource = this.createArbitraryDataResource();
|
||||||
|
|
||||||
this.preExecute();
|
this.preExecute();
|
||||||
this.deleteExistingFiles();
|
this.deleteExistingFiles();
|
||||||
this.fetch();
|
this.fetch();
|
||||||
@ -169,9 +180,18 @@ public class ArbitraryDataReader {
|
|||||||
this.uncompress();
|
this.uncompress();
|
||||||
this.validate();
|
this.validate();
|
||||||
|
|
||||||
} catch (DataException e) {
|
} catch (DataNotPublishedException e) {
|
||||||
|
if (e.getMessage() != null) {
|
||||||
|
// Log the message only, to avoid spamming the logs with a full stack trace
|
||||||
|
LOGGER.debug("DataNotPublishedException when trying to load QDN resource: {}", e.getMessage());
|
||||||
|
}
|
||||||
this.deleteWorkingDirectory();
|
this.deleteWorkingDirectory();
|
||||||
throw new DataException(e.getMessage());
|
throw e;
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.info("DataException when trying to load QDN resource", e);
|
||||||
|
this.deleteWorkingDirectory();
|
||||||
|
throw e;
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
this.postExecute();
|
this.postExecute();
|
||||||
@ -208,8 +228,13 @@ public class ArbitraryDataReader {
|
|||||||
* serve a cached version of the resource for subsequent requests.
|
* serve a cached version of the resource for subsequent requests.
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
private void deleteWorkingDirectory() throws IOException {
|
private void deleteWorkingDirectory() {
|
||||||
FilesystemUtils.safeDeleteDirectory(this.workingPath, true);
|
try {
|
||||||
|
FilesystemUtils.safeDeleteDirectory(this.workingPath, true);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Ignore failures as this isn't an essential step
|
||||||
|
LOGGER.info("Unable to delete working path {}: {}", this.workingPath, e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createUncompressedDirectory() throws DataException {
|
private void createUncompressedDirectory() throws DataException {
|
||||||
@ -408,6 +433,7 @@ public class ArbitraryDataReader {
|
|||||||
this.decryptUsingAlgo("AES/CBC/PKCS5Padding");
|
this.decryptUsingAlgo("AES/CBC/PKCS5Padding");
|
||||||
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
|
LOGGER.info("Unable to decrypt using specific parameters: {}", e.getMessage());
|
||||||
// Something went wrong, so fall back to default AES params (necessary for legacy resource support)
|
// Something went wrong, so fall back to default AES params (necessary for legacy resource support)
|
||||||
this.decryptUsingAlgo("AES");
|
this.decryptUsingAlgo("AES");
|
||||||
|
|
||||||
@ -420,8 +446,9 @@ public class ArbitraryDataReader {
|
|||||||
byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null;
|
byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null;
|
||||||
if (secret != null && secret.length == Transformer.AES256_LENGTH) {
|
if (secret != null && secret.length == Transformer.AES256_LENGTH) {
|
||||||
try {
|
try {
|
||||||
|
LOGGER.debug("Decrypting {} using algorithm {}...", this.arbitraryDataResource, algorithm);
|
||||||
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
|
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
|
||||||
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, algorithm);
|
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES");
|
||||||
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
|
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
|
||||||
|
|
||||||
// Replace filePath pointer with the encrypted file path
|
// Replace filePath pointer with the encrypted file path
|
||||||
@ -430,7 +457,8 @@ public class ArbitraryDataReader {
|
|||||||
|
|
||||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
|
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
|
||||||
| BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) {
|
| BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) {
|
||||||
throw new DataException(String.format("Unable to decrypt file at path %s: %s", this.filePath, e.getMessage()));
|
LOGGER.info(String.format("Exception when decrypting %s using algorithm %s", this.arbitraryDataResource, algorithm), e);
|
||||||
|
throw new DataException(String.format("Unable to decrypt file at path %s using algorithm %s: %s", this.filePath, algorithm, e.getMessage()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Assume it is unencrypted. This will be the case when we have built a custom path by combining
|
// Assume it is unencrypted. This will be the case when we have built a custom path by combining
|
||||||
@ -477,7 +505,12 @@ public class ArbitraryDataReader {
|
|||||||
// Delete original compressed file
|
// Delete original compressed file
|
||||||
if (FilesystemUtils.pathInsideDataOrTempPath(this.filePath)) {
|
if (FilesystemUtils.pathInsideDataOrTempPath(this.filePath)) {
|
||||||
if (Files.exists(this.filePath)) {
|
if (Files.exists(this.filePath)) {
|
||||||
Files.delete(this.filePath);
|
try {
|
||||||
|
Files.delete(this.filePath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Ignore failures as this isn't an essential step
|
||||||
|
LOGGER.info("Unable to delete file at path {}", this.filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.arbitrary;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||||
|
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||||
@ -325,7 +326,7 @@ public class ArbitraryDataResource {
|
|||||||
if (latestPut == null) {
|
if (latestPut == null) {
|
||||||
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
||||||
this.resourceId, this.service, this.identifierString());
|
this.resourceId, this.service, this.identifierString());
|
||||||
throw new DataException(message);
|
throw new DataNotPublishedException(message);
|
||||||
}
|
}
|
||||||
this.latestPutTransaction = latestPut;
|
this.latestPutTransaction = latestPut;
|
||||||
|
|
||||||
|
@ -23,16 +23,13 @@ import javax.crypto.NoSuchPaddingException;
|
|||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.*;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Iterator;
|
import java.util.stream.Collectors;
|
||||||
import java.util.List;
|
import java.util.stream.Stream;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class ArbitraryDataWriter {
|
public class ArbitraryDataWriter {
|
||||||
|
|
||||||
@ -50,6 +47,7 @@ public class ArbitraryDataWriter {
|
|||||||
private final String description;
|
private final String description;
|
||||||
private final List<String> tags;
|
private final List<String> tags;
|
||||||
private final Category category;
|
private final Category category;
|
||||||
|
private List<String> files;
|
||||||
|
|
||||||
private int chunkSize = ArbitraryDataFile.CHUNK_SIZE;
|
private int chunkSize = ArbitraryDataFile.CHUNK_SIZE;
|
||||||
|
|
||||||
@ -80,12 +78,14 @@ public class ArbitraryDataWriter {
|
|||||||
this.description = ArbitraryDataTransactionMetadata.limitDescription(description);
|
this.description = ArbitraryDataTransactionMetadata.limitDescription(description);
|
||||||
this.tags = ArbitraryDataTransactionMetadata.limitTags(tags);
|
this.tags = ArbitraryDataTransactionMetadata.limitTags(tags);
|
||||||
this.category = category;
|
this.category = category;
|
||||||
|
this.files = new ArrayList<>(); // Populated in buildFileList()
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() throws IOException, DataException, InterruptedException, MissingDataException {
|
public void save() throws IOException, DataException, InterruptedException, MissingDataException {
|
||||||
try {
|
try {
|
||||||
this.preExecute();
|
this.preExecute();
|
||||||
this.validateService();
|
this.validateService();
|
||||||
|
this.buildFileList();
|
||||||
this.process();
|
this.process();
|
||||||
this.compress();
|
this.compress();
|
||||||
this.encrypt();
|
this.encrypt();
|
||||||
@ -143,6 +143,24 @@ public class ArbitraryDataWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void buildFileList() throws IOException {
|
||||||
|
// Single file resources consist of a single element in the file list
|
||||||
|
boolean isSingleFile = this.filePath.toFile().isFile();
|
||||||
|
if (isSingleFile) {
|
||||||
|
this.files.add(this.filePath.getFileName().toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi file resources require a walk through the directory tree
|
||||||
|
try (Stream<Path> stream = Files.walk(this.filePath)) {
|
||||||
|
this.files = stream
|
||||||
|
.filter(Files::isRegularFile)
|
||||||
|
.map(p -> this.filePath.relativize(p).toString())
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void process() throws DataException, IOException, MissingDataException {
|
private void process() throws DataException, IOException, MissingDataException {
|
||||||
switch (this.method) {
|
switch (this.method) {
|
||||||
|
|
||||||
@ -285,6 +303,7 @@ public class ArbitraryDataWriter {
|
|||||||
metadata.setTags(this.tags);
|
metadata.setTags(this.tags);
|
||||||
metadata.setCategory(this.category);
|
metadata.setCategory(this.category);
|
||||||
metadata.setChunks(this.arbitraryDataFile.chunkHashList());
|
metadata.setChunks(this.arbitraryDataFile.chunkHashList());
|
||||||
|
metadata.setFiles(this.files);
|
||||||
metadata.write();
|
metadata.write();
|
||||||
|
|
||||||
// Create an ArbitraryDataFile from the JSON file (we don't have a signature yet)
|
// Create an ArbitraryDataFile from the JSON file (we don't have a signature yet)
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.qortal.arbitrary.exception;
|
||||||
|
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
|
||||||
|
public class DataNotPublishedException extends DataException {
|
||||||
|
|
||||||
|
public DataNotPublishedException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataNotPublishedException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataNotPublishedException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataNotPublishedException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -19,6 +19,7 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
|||||||
private String description;
|
private String description;
|
||||||
private List<String> tags;
|
private List<String> tags;
|
||||||
private Category category;
|
private Category category;
|
||||||
|
private List<String> files;
|
||||||
|
|
||||||
private static int MAX_TITLE_LENGTH = 80;
|
private static int MAX_TITLE_LENGTH = 80;
|
||||||
private static int MAX_DESCRIPTION_LENGTH = 500;
|
private static int MAX_DESCRIPTION_LENGTH = 500;
|
||||||
@ -77,6 +78,20 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
|||||||
}
|
}
|
||||||
this.chunks = chunksList;
|
this.chunks = chunksList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> filesList = new ArrayList<>();
|
||||||
|
if (metadata.has("files")) {
|
||||||
|
JSONArray files = metadata.getJSONArray("files");
|
||||||
|
if (files != null) {
|
||||||
|
for (int i=0; i<files.length(); i++) {
|
||||||
|
String tag = files.getString(i);
|
||||||
|
if (tag != null) {
|
||||||
|
filesList.add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.files = filesList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -111,6 +126,14 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
|||||||
}
|
}
|
||||||
outer.put("chunks", chunks);
|
outer.put("chunks", chunks);
|
||||||
|
|
||||||
|
JSONArray files = new JSONArray();
|
||||||
|
if (this.files != null) {
|
||||||
|
for (String file : this.files) {
|
||||||
|
files.put(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outer.put("files", files);
|
||||||
|
|
||||||
this.jsonString = outer.toString(2);
|
this.jsonString = outer.toString(2);
|
||||||
LOGGER.trace("Transaction metadata: {}", this.jsonString);
|
LOGGER.trace("Transaction metadata: {}", this.jsonString);
|
||||||
}
|
}
|
||||||
@ -156,6 +179,14 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
|||||||
return this.category;
|
return this.category;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFiles(List<String> files) {
|
||||||
|
this.files = files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getFiles() {
|
||||||
|
return this.files;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean containsChunk(byte[] chunk) {
|
public boolean containsChunk(byte[] chunk) {
|
||||||
for (byte[] c : this.chunks) {
|
for (byte[] c : this.chunks) {
|
||||||
if (Arrays.equals(c, chunk)) {
|
if (Arrays.equals(c, chunk)) {
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
package org.qortal.arbitrary.misc;
|
package org.qortal.arbitrary.misc;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.qortal.arbitrary.ArbitraryDataRenderer;
|
import org.qortal.arbitrary.ArbitraryDataRenderer;
|
||||||
import org.qortal.transaction.Transaction;
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.utils.FilesystemUtils;
|
import org.qortal.utils.FilesystemUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static java.util.Arrays.stream;
|
import static java.util.Arrays.stream;
|
||||||
import static java.util.stream.Collectors.toMap;
|
import static java.util.stream.Collectors.toMap;
|
||||||
@ -18,9 +18,52 @@ import static java.util.stream.Collectors.toMap;
|
|||||||
public enum Service {
|
public enum Service {
|
||||||
AUTO_UPDATE(1, false, null, null),
|
AUTO_UPDATE(1, false, null, null),
|
||||||
ARBITRARY_DATA(100, false, null, null),
|
ARBITRARY_DATA(100, false, null, null),
|
||||||
|
QCHAT_ATTACHMENT(120, true, 1024*1024L, null) {
|
||||||
|
@Override
|
||||||
|
public ValidationResult validate(Path path) throws IOException {
|
||||||
|
ValidationResult superclassResult = super.validate(path);
|
||||||
|
if (superclassResult != ValidationResult.OK) {
|
||||||
|
return superclassResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validation function to require a single file, with a whitelisted extension
|
||||||
|
int fileCount = 0;
|
||||||
|
File[] files = path.toFile().listFiles();
|
||||||
|
// If already a single file, replace the list with one that contains that file only
|
||||||
|
if (files == null && path.toFile().isFile()) {
|
||||||
|
files = new File[] { path.toFile() };
|
||||||
|
}
|
||||||
|
if (files != null) {
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.getName().equals(".qortal")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
||||||
|
}
|
||||||
|
final String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
|
||||||
|
// We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
|
||||||
|
final List<String> allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
|
||||||
|
if (extension == null || !allowedExtensions.contains(extension)) {
|
||||||
|
return ValidationResult.INVALID_FILE_EXTENSION;
|
||||||
|
}
|
||||||
|
fileCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileCount != 1) {
|
||||||
|
return ValidationResult.INVALID_FILE_COUNT;
|
||||||
|
}
|
||||||
|
return ValidationResult.OK;
|
||||||
|
}
|
||||||
|
},
|
||||||
WEBSITE(200, true, null, null) {
|
WEBSITE(200, true, null, null) {
|
||||||
@Override
|
@Override
|
||||||
public ValidationResult validate(Path path) {
|
public ValidationResult validate(Path path) throws IOException {
|
||||||
|
ValidationResult superclassResult = super.validate(path);
|
||||||
|
if (superclassResult != ValidationResult.OK) {
|
||||||
|
return superclassResult;
|
||||||
|
}
|
||||||
|
|
||||||
// Custom validation function to require an index HTML file in the root directory
|
// Custom validation function to require an index HTML file in the root directory
|
||||||
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
|
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
|
||||||
String[] files = path.toFile().list();
|
String[] files = path.toFile().list();
|
||||||
@ -38,6 +81,7 @@ public enum Service {
|
|||||||
GIT_REPOSITORY(300, false, null, null),
|
GIT_REPOSITORY(300, false, null, null),
|
||||||
IMAGE(400, true, 10*1024*1024L, null),
|
IMAGE(400, true, 10*1024*1024L, null),
|
||||||
THUMBNAIL(410, true, 500*1024L, null),
|
THUMBNAIL(410, true, 500*1024L, null),
|
||||||
|
QCHAT_IMAGE(420, true, 500*1024L, null),
|
||||||
VIDEO(500, false, null, null),
|
VIDEO(500, false, null, null),
|
||||||
AUDIO(600, false, null, null),
|
AUDIO(600, false, null, null),
|
||||||
BLOG(700, false, null, null),
|
BLOG(700, false, null, null),
|
||||||
@ -48,7 +92,42 @@ public enum Service {
|
|||||||
PLAYLIST(910, true, null, null),
|
PLAYLIST(910, true, null, null),
|
||||||
APP(1000, false, null, null),
|
APP(1000, false, null, null),
|
||||||
METADATA(1100, false, null, null),
|
METADATA(1100, false, null, null),
|
||||||
QORTAL_METADATA(1111, true, 10*1024L, Arrays.asList("title", "description", "tags"));
|
GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
|
||||||
|
@Override
|
||||||
|
public ValidationResult validate(Path path) throws IOException {
|
||||||
|
ValidationResult superclassResult = super.validate(path);
|
||||||
|
if (superclassResult != ValidationResult.OK) {
|
||||||
|
return superclassResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validation function to require .gif files only, and at least 1
|
||||||
|
int gifCount = 0;
|
||||||
|
File[] files = path.toFile().listFiles();
|
||||||
|
// If already a single file, replace the list with one that contains that file only
|
||||||
|
if (files == null && path.toFile().isFile()) {
|
||||||
|
files = new File[] { path.toFile() };
|
||||||
|
}
|
||||||
|
if (files != null) {
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.getName().equals(".qortal")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
||||||
|
}
|
||||||
|
String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
|
||||||
|
if (!Objects.equals(extension, "gif")) {
|
||||||
|
return ValidationResult.INVALID_FILE_EXTENSION;
|
||||||
|
}
|
||||||
|
gifCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gifCount == 0) {
|
||||||
|
return ValidationResult.MISSING_DATA;
|
||||||
|
}
|
||||||
|
return ValidationResult.OK;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
private final boolean requiresValidation;
|
private final boolean requiresValidation;
|
||||||
@ -114,7 +193,11 @@ public enum Service {
|
|||||||
OK(1),
|
OK(1),
|
||||||
MISSING_KEYS(2),
|
MISSING_KEYS(2),
|
||||||
EXCEEDS_SIZE_LIMIT(3),
|
EXCEEDS_SIZE_LIMIT(3),
|
||||||
MISSING_INDEX_FILE(4);
|
MISSING_INDEX_FILE(4),
|
||||||
|
DIRECTORIES_NOT_ALLOWED(5),
|
||||||
|
INVALID_FILE_EXTENSION(6),
|
||||||
|
MISSING_DATA(7),
|
||||||
|
INVALID_FILE_COUNT(8);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import org.qortal.block.BlockChain.BlockTimingByHeight;
|
|||||||
import org.qortal.block.BlockChain.AccountLevelShareBin;
|
import org.qortal.block.BlockChain.AccountLevelShareBin;
|
||||||
import org.qortal.controller.OnlineAccountsManager;
|
import org.qortal.controller.OnlineAccountsManager;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.crypto.Qortal25519Extras;
|
||||||
import org.qortal.data.account.AccountBalanceData;
|
import org.qortal.data.account.AccountBalanceData;
|
||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
import org.qortal.data.account.EligibleQoraHolderData;
|
import org.qortal.data.account.EligibleQoraHolderData;
|
||||||
@ -88,7 +89,8 @@ public class Block {
|
|||||||
ONLINE_ACCOUNT_UNKNOWN(71),
|
ONLINE_ACCOUNT_UNKNOWN(71),
|
||||||
ONLINE_ACCOUNT_SIGNATURES_MISSING(72),
|
ONLINE_ACCOUNT_SIGNATURES_MISSING(72),
|
||||||
ONLINE_ACCOUNT_SIGNATURES_MALFORMED(73),
|
ONLINE_ACCOUNT_SIGNATURES_MALFORMED(73),
|
||||||
ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74);
|
ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74),
|
||||||
|
ONLINE_ACCOUNT_NONCE_INCORRECT(75);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
|
|
||||||
@ -134,7 +136,7 @@ public class Block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Lazy-instantiated expanded info on block's online accounts. */
|
/** Lazy-instantiated expanded info on block's online accounts. */
|
||||||
private static class ExpandedAccount {
|
public static class ExpandedAccount {
|
||||||
private final RewardShareData rewardShareData;
|
private final RewardShareData rewardShareData;
|
||||||
private final int sharePercent;
|
private final int sharePercent;
|
||||||
private final boolean isRecipientAlsoMinter;
|
private final boolean isRecipientAlsoMinter;
|
||||||
@ -167,6 +169,13 @@ public class Block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Account getMintingAccount() {
|
||||||
|
return this.mintingAccount;
|
||||||
|
}
|
||||||
|
public Account getRecipientAccount() {
|
||||||
|
return this.recipientAccount;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns share bin for expanded account.
|
* Returns share bin for expanded account.
|
||||||
* <p>
|
* <p>
|
||||||
@ -183,8 +192,11 @@ public class Block {
|
|||||||
if (accountLevel <= 0)
|
if (accountLevel <= 0)
|
||||||
return null; // level 0 isn't included in any share bins
|
return null; // level 0 isn't included in any share bins
|
||||||
|
|
||||||
|
// Select the correct set of share bins based on block height
|
||||||
final BlockChain blockChain = BlockChain.getInstance();
|
final BlockChain blockChain = BlockChain.getInstance();
|
||||||
final AccountLevelShareBin[] shareBinsByLevel = blockChain.getShareBinsByAccountLevel();
|
final AccountLevelShareBin[] shareBinsByLevel = (blockHeight >= blockChain.getSharesByLevelV2Height()) ?
|
||||||
|
blockChain.getShareBinsByAccountLevelV2() : blockChain.getShareBinsByAccountLevelV1();
|
||||||
|
|
||||||
if (accountLevel > shareBinsByLevel.length)
|
if (accountLevel > shareBinsByLevel.length)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -197,6 +209,11 @@ public class Block {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasShareBin(AccountLevelShareBin shareBin, int blockHeight) {
|
||||||
|
AccountLevelShareBin ourShareBin = this.getShareBin(blockHeight);
|
||||||
|
return ourShareBin != null && shareBin.id == ourShareBin.id;
|
||||||
|
}
|
||||||
|
|
||||||
public long distribute(long accountAmount, Map<String, Long> balanceChanges) {
|
public long distribute(long accountAmount, Map<String, Long> balanceChanges) {
|
||||||
if (this.isRecipientAlsoMinter) {
|
if (this.isRecipientAlsoMinter) {
|
||||||
// minter & recipient the same - simpler case
|
// minter & recipient the same - simpler case
|
||||||
@ -221,11 +238,10 @@ public class Block {
|
|||||||
return accountAmount;
|
return accountAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
|
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
|
||||||
private List<ExpandedAccount> cachedExpandedAccounts = null;
|
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(). */
|
/** Opportunistic cache of this block's valid online reward-shares. Only created by call to isValid(). */
|
||||||
private List<RewardShareData> cachedOnlineRewardShares = null;
|
private List<RewardShareData> cachedOnlineRewardShares = null;
|
||||||
|
|
||||||
@ -347,18 +363,36 @@ public class Block {
|
|||||||
int version = parentBlock.getNextBlockVersion();
|
int version = parentBlock.getNextBlockVersion();
|
||||||
byte[] reference = parentBlockData.getSignature();
|
byte[] reference = parentBlockData.getSignature();
|
||||||
|
|
||||||
// Fetch our list of online accounts
|
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
|
||||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
|
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
|
||||||
if (onlineAccounts.isEmpty()) {
|
if (minterLevel == 0) {
|
||||||
LOGGER.error("No online accounts - not even our own?");
|
LOGGER.error("Minter effective level returned zero?");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find newest online accounts timestamp
|
int height = parentBlockData.getHeight() + 1;
|
||||||
long onlineAccountsTimestamp = 0;
|
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
|
||||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
|
||||||
if (onlineAccountData.getTimestamp() > onlineAccountsTimestamp)
|
|
||||||
onlineAccountsTimestamp = onlineAccountData.getTimestamp();
|
// Fetch our list of online accounts, removing any that are missing a nonce
|
||||||
|
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
|
||||||
|
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
|
||||||
|
|
||||||
|
// After feature trigger, remove any online accounts that are level 0
|
||||||
|
if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||||
|
onlineAccounts.removeIf(a -> {
|
||||||
|
try {
|
||||||
|
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
|
||||||
|
} catch (DataException e) {
|
||||||
|
// Something went wrong, so remove the account
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onlineAccounts.isEmpty()) {
|
||||||
|
LOGGER.debug("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.
|
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
|
||||||
@ -369,10 +403,6 @@ public class Block {
|
|||||||
// Map using index into sorted list of reward-shares as key
|
// Map using index into sorted list of reward-shares as key
|
||||||
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
|
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
|
||||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||||
// Disregard online accounts with different timestamps
|
|
||||||
if (onlineAccountData.getTimestamp() != onlineAccountsTimestamp)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
|
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
|
||||||
if (accountIndex == null)
|
if (accountIndex == null)
|
||||||
// Online account (reward-share) with current timestamp but reward-share cancelled
|
// Online account (reward-share) with current timestamp but reward-share cancelled
|
||||||
@ -389,29 +419,43 @@ public class Block {
|
|||||||
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
|
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
|
||||||
int onlineAccountsCount = onlineAccountsSet.size();
|
int onlineAccountsCount = onlineAccountsSet.size();
|
||||||
|
|
||||||
// Concatenate online account timestamp signatures (in correct order)
|
// Collate all signatures
|
||||||
byte[] onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH];
|
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
|
||||||
for (int i = 0; i < onlineAccountsCount; ++i) {
|
.stream()
|
||||||
Integer accountIndex = accountIndexes.get(i);
|
.map(OnlineAccountData::getSignature)
|
||||||
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
|
.collect(Collectors.toList());
|
||||||
System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
|
|
||||||
|
// Aggregated, single signature
|
||||||
|
byte[] onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
|
||||||
|
|
||||||
|
// Add nonces to the end of the online accounts signatures
|
||||||
|
try {
|
||||||
|
// Create ordered list of nonce values
|
||||||
|
List<Integer> nonces = new ArrayList<>();
|
||||||
|
for (int i = 0; i < onlineAccountsCount; ++i) {
|
||||||
|
Integer accountIndex = accountIndexes.get(i);
|
||||||
|
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
|
||||||
|
nonces.add(onlineAccountData.getNonce());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the nonces to a byte array
|
||||||
|
byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
|
||||||
|
|
||||||
|
// Append the encoded nonces to the encoded online account signatures
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
outputStream.write(onlineAccountsSignatures);
|
||||||
|
outputStream.write(encodedNonces);
|
||||||
|
onlineAccountsSignatures = outputStream.toByteArray();
|
||||||
|
}
|
||||||
|
catch (TransformationException | IOException e) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
|
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
|
||||||
minter.getPublicKey(), encodedOnlineAccounts));
|
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;
|
int transactionCount = 0;
|
||||||
byte[] transactionsSignature = null;
|
byte[] transactionsSignature = null;
|
||||||
int height = parentBlockData.getHeight() + 1;
|
|
||||||
|
|
||||||
int atCount = 0;
|
int atCount = 0;
|
||||||
long atFees = 0;
|
long atFees = 0;
|
||||||
@ -1005,6 +1049,15 @@ public class Block {
|
|||||||
if (onlineRewardShares == null)
|
if (onlineRewardShares == null)
|
||||||
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
||||||
|
|
||||||
|
// After feature trigger, require all online account minters to be greater than level 0
|
||||||
|
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||||
|
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||||
|
for (ExpandedAccount account : expandedAccounts) {
|
||||||
|
if (account.getMintingAccount().getEffectiveMintingLevel() == 0)
|
||||||
|
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If block is past a certain age then we simply assume the signatures were correct
|
// If block is past a certain age then we simply assume the signatures were correct
|
||||||
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
||||||
if (this.blockData.getTimestamp() < signatureRequirementThreshold)
|
if (this.blockData.getTimestamp() < signatureRequirementThreshold)
|
||||||
@ -1013,49 +1066,64 @@ public class Block {
|
|||||||
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
|
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
|
||||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
|
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
|
||||||
|
|
||||||
if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
|
final int signaturesLength = Transformer.SIGNATURE_LENGTH;
|
||||||
|
final int noncesLength = onlineRewardShares.size() * Transformer.INT_LENGTH;
|
||||||
|
|
||||||
|
// We expect nonces to be appended to the online accounts signatures
|
||||||
|
if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength + noncesLength)
|
||||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
||||||
|
|
||||||
// Check signatures
|
// Check signatures
|
||||||
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
|
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
|
||||||
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);
|
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);
|
||||||
|
|
||||||
// If this block is much older than current online timestamp, then there's no point checking current online accounts
|
byte[] encodedOnlineAccountSignatures = this.blockData.getOnlineAccountsSignatures();
|
||||||
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
|
// Split online account signatures into signature(s) + nonces, then validate the nonces
|
||||||
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
|
byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength);
|
||||||
|
byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH);
|
||||||
|
encodedOnlineAccountSignatures = extractedSignatures;
|
||||||
|
|
||||||
// We'll build up a list of online accounts to hand over to Controller if block is added to chain
|
List<Integer> nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
|
||||||
// and this will become latestBlocksOnlineAccounts (above) to reduce CPU load when we process next block...
|
|
||||||
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
|
|
||||||
|
|
||||||
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
|
// Build block's view of online accounts (without signatures, as we don't need them here)
|
||||||
byte[] signature = onlineAccountsSignatures.get(i);
|
Set<OnlineAccountData> onlineAccounts = new HashSet<>();
|
||||||
|
for (int i = 0; i < onlineRewardShares.size(); ++i) {
|
||||||
|
Integer nonce = nonces.get(i);
|
||||||
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
|
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
|
||||||
|
|
||||||
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
|
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, null, publicKey, nonce);
|
||||||
ourOnlineAccounts.add(onlineAccountData);
|
onlineAccounts.add(onlineAccountData);
|
||||||
|
|
||||||
// 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))
|
|
||||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (!OnlineAccountsManager.getInstance().verifyMemoryPoW(onlineAccount, null))
|
||||||
|
return ValidationResult.ONLINE_ACCOUNT_NONCE_INCORRECT;
|
||||||
|
|
||||||
|
// Cache the valid online accounts as they will likely be needed for the next block
|
||||||
|
OnlineAccountsManager.getInstance().addBlocksOnlineAccounts(onlineAccounts, onlineTimestamp);
|
||||||
|
|
||||||
|
// Extract online accounts' timestamp signatures from block data. Only one signature if aggregated.
|
||||||
|
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(encodedOnlineAccountSignatures);
|
||||||
|
|
||||||
|
// Aggregate all public keys
|
||||||
|
Collection<byte[]> publicKeys = onlineRewardShares.stream()
|
||||||
|
.map(RewardShareData::getRewardSharePublicKey)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
byte[] aggregatePublicKey = Qortal25519Extras.aggregatePublicKeys(publicKeys);
|
||||||
|
|
||||||
|
byte[] aggregateSignature = onlineAccountsSignatures.get(0);
|
||||||
|
|
||||||
|
// One-step verification of aggregate signature using aggregate public key
|
||||||
|
if (!Qortal25519Extras.verifyAggregated(aggregatePublicKey, aggregateSignature, onlineTimestampBytes))
|
||||||
|
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
|
||||||
|
|
||||||
// All online accounts valid, so save our list of online accounts for potential later use
|
// All online accounts valid, so save our list of online accounts for potential later use
|
||||||
this.cachedValidOnlineAccounts = ourOnlineAccounts;
|
|
||||||
this.cachedOnlineRewardShares = onlineRewardShares;
|
this.cachedOnlineRewardShares = onlineRewardShares;
|
||||||
|
|
||||||
return ValidationResult.OK;
|
return ValidationResult.OK;
|
||||||
@ -1202,6 +1270,7 @@ public class Block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
|
LOGGER.info("DataException during transaction validation", e);
|
||||||
return ValidationResult.TRANSACTION_INVALID;
|
return ValidationResult.TRANSACTION_INVALID;
|
||||||
} finally {
|
} finally {
|
||||||
// Rollback repository changes made by test-processing transactions above
|
// Rollback repository changes made by test-processing transactions above
|
||||||
@ -1394,6 +1463,9 @@ public class Block {
|
|||||||
if (this.blockData.getHeight() == 212937)
|
if (this.blockData.getHeight() == 212937)
|
||||||
// Apply fix for block 212937
|
// Apply fix for block 212937
|
||||||
Block212937.processFix(this);
|
Block212937.processFix(this);
|
||||||
|
|
||||||
|
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||||
|
SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're about to (test-)process a batch of transactions,
|
// We're about to (test-)process a batch of transactions,
|
||||||
@ -1426,9 +1498,6 @@ public class Block {
|
|||||||
|
|
||||||
postBlockTidy();
|
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
|
// Log some debugging info relating to the block weight calculation
|
||||||
this.logDebugInfo();
|
this.logDebugInfo();
|
||||||
}
|
}
|
||||||
@ -1453,19 +1522,23 @@ public class Block {
|
|||||||
// Batch update in repository
|
// Batch update in repository
|
||||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
|
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
|
||||||
|
|
||||||
|
// Keep track of level bumps in case we need to apply to other entries
|
||||||
|
Map<String, Integer> bumpedAccounts = new HashMap<>();
|
||||||
|
|
||||||
// Local changes and also checks for level bump
|
// Local changes and also checks for level bump
|
||||||
for (AccountData accountData : allUniqueExpandedAccounts) {
|
for (AccountData accountData : allUniqueExpandedAccounts) {
|
||||||
// Adjust count locally (in Java)
|
// Adjust count locally (in Java)
|
||||||
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
||||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||||
|
|
||||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||||
|
|
||||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||||
if (newLevel > accountData.getLevel()) {
|
if (newLevel > accountData.getLevel()) {
|
||||||
// Account has increased in level!
|
// Account has increased in level!
|
||||||
accountData.setLevel(newLevel);
|
accountData.setLevel(newLevel);
|
||||||
|
bumpedAccounts.put(accountData.getAddress(), newLevel);
|
||||||
repository.getAccountRepository().setLevel(accountData);
|
repository.getAccountRepository().setLevel(accountData);
|
||||||
LOGGER.trace(() -> String.format("Block minter %s bumped to level %d", accountData.getAddress(), accountData.getLevel()));
|
LOGGER.trace(() -> String.format("Block minter %s bumped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||||
}
|
}
|
||||||
@ -1473,6 +1546,25 @@ public class Block {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also bump other entries if need be
|
||||||
|
if (!bumpedAccounts.isEmpty()) {
|
||||||
|
for (ExpandedAccount expandedAccount : expandedAccounts) {
|
||||||
|
Integer newLevel = bumpedAccounts.get(expandedAccount.mintingAccountData.getAddress());
|
||||||
|
if (newLevel != null && expandedAccount.mintingAccountData.getLevel() != newLevel) {
|
||||||
|
expandedAccount.mintingAccountData.setLevel(newLevel);
|
||||||
|
LOGGER.trace("Also bumped {} to level {}", expandedAccount.mintingAccountData.getAddress(), newLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expandedAccount.isRecipientAlsoMinter) {
|
||||||
|
newLevel = bumpedAccounts.get(expandedAccount.recipientAccountData.getAddress());
|
||||||
|
if (newLevel != null && expandedAccount.recipientAccountData.getLevel() != newLevel) {
|
||||||
|
expandedAccount.recipientAccountData.setLevel(newLevel);
|
||||||
|
LOGGER.trace("Also bumped {} to level {}", expandedAccount.recipientAccountData.getAddress(), newLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void processBlockRewards() throws DataException {
|
protected void processBlockRewards() throws DataException {
|
||||||
@ -1632,6 +1724,9 @@ public class Block {
|
|||||||
// Revert fix for block 212937
|
// Revert fix for block 212937
|
||||||
Block212937.orphanFix(this);
|
Block212937.orphanFix(this);
|
||||||
|
|
||||||
|
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||||
|
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
|
||||||
|
|
||||||
// Block rewards, including transaction fees, removed after transactions undone
|
// Block rewards, including transaction fees, removed after transactions undone
|
||||||
orphanBlockRewards();
|
orphanBlockRewards();
|
||||||
|
|
||||||
@ -1644,9 +1739,6 @@ public class Block {
|
|||||||
this.blockData.setHeight(null);
|
this.blockData.setHeight(null);
|
||||||
|
|
||||||
postBlockTidy();
|
postBlockTidy();
|
||||||
|
|
||||||
// Remove any cached, valid online accounts data from Controller
|
|
||||||
OnlineAccountsManager.getInstance().popLatestBlocksOnlineAccounts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void orphanTransactionsFromBlock() throws DataException {
|
protected void orphanTransactionsFromBlock() throws DataException {
|
||||||
@ -1763,7 +1855,7 @@ public class Block {
|
|||||||
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
||||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||||
|
|
||||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||||
|
|
||||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||||
@ -1883,13 +1975,72 @@ public class Block {
|
|||||||
final List<ExpandedAccount> onlineFounderAccounts = expandedAccounts.stream().filter(expandedAccount -> expandedAccount.isMinterFounder).collect(Collectors.toList());
|
final List<ExpandedAccount> onlineFounderAccounts = expandedAccounts.stream().filter(expandedAccount -> expandedAccount.isMinterFounder).collect(Collectors.toList());
|
||||||
final boolean haveFounders = !onlineFounderAccounts.isEmpty();
|
final boolean haveFounders = !onlineFounderAccounts.isEmpty();
|
||||||
|
|
||||||
|
// Select the correct set of share bins based on block height
|
||||||
|
List<AccountLevelShareBin> accountLevelShareBinsForBlock = (this.blockData.getHeight() >= BlockChain.getInstance().getSharesByLevelV2Height()) ?
|
||||||
|
BlockChain.getInstance().getAccountLevelShareBinsV2() : BlockChain.getInstance().getAccountLevelShareBinsV1();
|
||||||
|
|
||||||
// Determine reward candidates based on account level
|
// Determine reward candidates based on account level
|
||||||
List<AccountLevelShareBin> accountLevelShareBins = BlockChain.getInstance().getAccountLevelShareBins();
|
// This needs a deep copy, so the shares can be modified when tiers aren't activated yet
|
||||||
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
|
List<AccountLevelShareBin> accountLevelShareBins = new ArrayList<>();
|
||||||
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
|
for (AccountLevelShareBin accountLevelShareBin : accountLevelShareBinsForBlock) {
|
||||||
|
accountLevelShareBins.add((AccountLevelShareBin) accountLevelShareBin.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Integer, List<ExpandedAccount>> accountsForShareBin = new HashMap<>();
|
||||||
|
|
||||||
|
// We might need to combine some share bins if they haven't reached the minimum number of minters yet
|
||||||
|
for (int binIndex = accountLevelShareBins.size()-1; binIndex >= 0; --binIndex) {
|
||||||
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
|
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
|
||||||
// Object reference compare is OK as all references are read-only from blockchain config.
|
|
||||||
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin(this.blockData.getHeight()) == accountLevelShareBin).collect(Collectors.toList());
|
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
|
||||||
|
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.hasShareBin(accountLevelShareBin, this.blockData.getHeight())).collect(Collectors.toList());
|
||||||
|
// Add any accounts that have been moved down from a higher tier
|
||||||
|
List<ExpandedAccount> existingBinnedAccounts = accountsForShareBin.get(binIndex);
|
||||||
|
if (existingBinnedAccounts != null)
|
||||||
|
binnedAccounts.addAll(existingBinnedAccounts);
|
||||||
|
|
||||||
|
// Logic below may only apply to higher levels, and only for share bins with a specific range of online accounts
|
||||||
|
if (accountLevelShareBin.levels.get(0) < BlockChain.getInstance().getShareBinActivationMinLevel() ||
|
||||||
|
binnedAccounts.isEmpty() || binnedAccounts.size() >= BlockChain.getInstance().getMinAccountsToActivateShareBin()) {
|
||||||
|
// Add all accounts for this share bin to the accountsForShareBin list
|
||||||
|
accountsForShareBin.put(binIndex, binnedAccounts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share bin contains more than one, but less than the minimum number of minters. We treat this share bin
|
||||||
|
// as not activated yet. In these cases, the rewards and minters are combined and paid out to the previous
|
||||||
|
// share bin, to prevent a single or handful of accounts receiving the entire rewards for a share bin.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// - Share bin for levels 5 and 6 has 100 minters
|
||||||
|
// - Share bin for levels 7 and 8 has 10 minters
|
||||||
|
//
|
||||||
|
// This is below the minimum of 30, so share bins are reconstructed as follows:
|
||||||
|
//
|
||||||
|
// - Share bin for levels 5 and 6 now contains 110 minters
|
||||||
|
// - Share bin for levels 7 and 8 now contains 0 minters
|
||||||
|
// - Share bin for levels 5 and 6 now pays out rewards for levels 5, 6, 7, and 8
|
||||||
|
// - Share bin for levels 7 and 8 pays zero rewards
|
||||||
|
//
|
||||||
|
// This process is iterative, so will combine several tiers if needed.
|
||||||
|
|
||||||
|
// Designate this share bin as empty
|
||||||
|
accountsForShareBin.put(binIndex, new ArrayList<>());
|
||||||
|
|
||||||
|
// Move the accounts originally intended for this share bin to the previous one
|
||||||
|
accountsForShareBin.put(binIndex - 1, binnedAccounts);
|
||||||
|
|
||||||
|
// Move the block reward from this share bin to the previous one
|
||||||
|
AccountLevelShareBin previousShareBin = accountLevelShareBins.get(binIndex - 1);
|
||||||
|
previousShareBin.share += accountLevelShareBin.share;
|
||||||
|
accountLevelShareBin.share = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now loop through (potentially modified) share bins and determine the reward candidates
|
||||||
|
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
|
||||||
|
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
|
||||||
|
List<ExpandedAccount> binnedAccounts = accountsForShareBin.get(binIndex);
|
||||||
|
|
||||||
// No online accounts in this bin? Skip to next one
|
// No online accounts in this bin? Skip to next one
|
||||||
if (binnedAccounts.isEmpty())
|
if (binnedAccounts.isEmpty())
|
||||||
@ -1907,7 +2058,7 @@ public class Block {
|
|||||||
// Fetch list of legacy QORA holders who haven't reached their cap of QORT reward.
|
// Fetch list of legacy QORA holders who haven't reached their cap of QORT reward.
|
||||||
List<EligibleQoraHolderData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
|
List<EligibleQoraHolderData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
|
||||||
final boolean haveQoraHolders = !qoraHolders.isEmpty();
|
final boolean haveQoraHolders = !qoraHolders.isEmpty();
|
||||||
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare();
|
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight());
|
||||||
|
|
||||||
// Perform account-level-based reward scaling if appropriate
|
// Perform account-level-based reward scaling if appropriate
|
||||||
if (!haveFounders) {
|
if (!haveFounders) {
|
||||||
|
@ -68,10 +68,17 @@ public class BlockChain {
|
|||||||
atFindNextTransactionFix,
|
atFindNextTransactionFix,
|
||||||
newBlockSigHeight,
|
newBlockSigHeight,
|
||||||
shareBinFix,
|
shareBinFix,
|
||||||
|
sharesByLevelV2Height,
|
||||||
|
rewardShareLimitTimestamp,
|
||||||
calcChainWeightTimestamp,
|
calcChainWeightTimestamp,
|
||||||
transactionV5Timestamp,
|
transactionV5Timestamp,
|
||||||
transactionV6Timestamp,
|
transactionV6Timestamp,
|
||||||
disableReferenceTimestamp
|
disableReferenceTimestamp,
|
||||||
|
increaseOnlineAccountsDifficultyTimestamp,
|
||||||
|
onlineAccountMinterLevelValidationHeight,
|
||||||
|
selfSponsorshipAlgoV1Height,
|
||||||
|
feeValidationFixTimestamp,
|
||||||
|
chatReferenceTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom transaction fees
|
// Custom transaction fees
|
||||||
@ -93,6 +100,13 @@ public class BlockChain {
|
|||||||
/** Whether only one registered name is allowed per account. */
|
/** Whether only one registered name is allowed per account. */
|
||||||
private boolean oneNamePerAccount = false;
|
private boolean oneNamePerAccount = false;
|
||||||
|
|
||||||
|
/** Checkpoints */
|
||||||
|
public static class Checkpoint {
|
||||||
|
public int height;
|
||||||
|
public String signature;
|
||||||
|
}
|
||||||
|
private List<Checkpoint> checkpoints;
|
||||||
|
|
||||||
/** Block rewards by block height */
|
/** Block rewards by block height */
|
||||||
public static class RewardByHeight {
|
public static class RewardByHeight {
|
||||||
public int height;
|
public int height;
|
||||||
@ -102,23 +116,48 @@ public class BlockChain {
|
|||||||
private List<RewardByHeight> rewardsByHeight;
|
private List<RewardByHeight> rewardsByHeight;
|
||||||
|
|
||||||
/** Share of block reward/fees by account level */
|
/** Share of block reward/fees by account level */
|
||||||
public static class AccountLevelShareBin {
|
public static class AccountLevelShareBin implements Cloneable {
|
||||||
|
public int id;
|
||||||
public List<Integer> levels;
|
public List<Integer> levels;
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
public long share;
|
public long share;
|
||||||
}
|
|
||||||
private List<AccountLevelShareBin> sharesByLevel;
|
|
||||||
/** Generated lookup of share-bin by account level */
|
|
||||||
private AccountLevelShareBin[] shareBinsByLevel;
|
|
||||||
|
|
||||||
/** Share of block reward/fees to legacy QORA coin holders */
|
public Object clone() {
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
AccountLevelShareBin shareBinCopy = new AccountLevelShareBin();
|
||||||
private Long qoraHoldersShare;
|
List<Integer> levelsCopy = new ArrayList<>();
|
||||||
|
for (Integer level : this.levels) {
|
||||||
|
levelsCopy.add(level);
|
||||||
|
}
|
||||||
|
shareBinCopy.id = this.id;
|
||||||
|
shareBinCopy.levels = levelsCopy;
|
||||||
|
shareBinCopy.share = this.share;
|
||||||
|
return shareBinCopy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private List<AccountLevelShareBin> sharesByLevelV1;
|
||||||
|
private List<AccountLevelShareBin> sharesByLevelV2;
|
||||||
|
/** Generated lookup of share-bin by account level */
|
||||||
|
private AccountLevelShareBin[] shareBinsByLevelV1;
|
||||||
|
private AccountLevelShareBin[] shareBinsByLevelV2;
|
||||||
|
|
||||||
|
/** Share of block reward/fees to legacy QORA coin holders, by block height */
|
||||||
|
public static class ShareByHeight {
|
||||||
|
public int height;
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public long share;
|
||||||
|
}
|
||||||
|
private List<ShareByHeight> qoraHoldersShareByHeight;
|
||||||
|
|
||||||
/** How many legacy QORA per 1 QORT of block reward. */
|
/** How many legacy QORA per 1 QORT of block reward. */
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
private Long qoraPerQortReward;
|
private Long qoraPerQortReward;
|
||||||
|
|
||||||
|
/** Minimum number of accounts before a share bin is considered activated */
|
||||||
|
private int minAccountsToActivateShareBin;
|
||||||
|
|
||||||
|
/** Min level at which share bin activation takes place; lower levels allow less than minAccountsPerShareBin */
|
||||||
|
private int shareBinActivationMinLevel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of minted blocks required to reach next level from previous.
|
* Number of minted blocks required to reach next level from previous.
|
||||||
* <p>
|
* <p>
|
||||||
@ -156,7 +195,7 @@ public class BlockChain {
|
|||||||
private int minAccountLevelToMint;
|
private int minAccountLevelToMint;
|
||||||
private int minAccountLevelForBlockSubmissions;
|
private int minAccountLevelForBlockSubmissions;
|
||||||
private int minAccountLevelToRewardShare;
|
private int minAccountLevelToRewardShare;
|
||||||
private int maxRewardSharesPerMintingAccount;
|
private int maxRewardSharesPerFounderMintingAccount;
|
||||||
private int founderEffectiveMintingLevel;
|
private int founderEffectiveMintingLevel;
|
||||||
|
|
||||||
/** Minimum time to retain online account signatures (ms) for block validity checks. */
|
/** Minimum time to retain online account signatures (ms) for block validity checks. */
|
||||||
@ -164,6 +203,20 @@ public class BlockChain {
|
|||||||
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
|
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
|
||||||
private long onlineAccountSignaturesMaxLifetime;
|
private long onlineAccountSignaturesMaxLifetime;
|
||||||
|
|
||||||
|
/** Feature trigger timestamp for ONLINE_ACCOUNTS_MODULUS time interval increase. Can't use
|
||||||
|
* featureTriggers because unit tests need to set this value via Reflection. */
|
||||||
|
private long onlineAccountsModulusV2Timestamp;
|
||||||
|
|
||||||
|
/** Snapshot timestamp for self sponsorship algo V1 */
|
||||||
|
private long selfSponsorshipAlgoV1SnapshotTimestamp;
|
||||||
|
|
||||||
|
/** 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. */
|
/** Settings relating to CIYAM AT feature. */
|
||||||
public static class CiyamAtSettings {
|
public static class CiyamAtSettings {
|
||||||
/** Fee per step/op-code executed. */
|
/** Fee per step/op-code executed. */
|
||||||
@ -312,6 +365,16 @@ public class BlockChain {
|
|||||||
return this.maxBlockSize;
|
return this.maxBlockSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Online accounts
|
||||||
|
public long getOnlineAccountsModulusV2Timestamp() {
|
||||||
|
return this.onlineAccountsModulusV2Timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Self sponsorship algo
|
||||||
|
public long getSelfSponsorshipAlgoV1SnapshotTimestamp() {
|
||||||
|
return this.selfSponsorshipAlgoV1SnapshotTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
||||||
public boolean getRequireGroupForApproval() {
|
public boolean getRequireGroupForApproval() {
|
||||||
return this.requireGroupForApproval;
|
return this.requireGroupForApproval;
|
||||||
@ -325,16 +388,28 @@ public class BlockChain {
|
|||||||
return this.oneNamePerAccount;
|
return this.oneNamePerAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Checkpoint> getCheckpoints() {
|
||||||
|
return this.checkpoints;
|
||||||
|
}
|
||||||
|
|
||||||
public List<RewardByHeight> getBlockRewardsByHeight() {
|
public List<RewardByHeight> getBlockRewardsByHeight() {
|
||||||
return this.rewardsByHeight;
|
return this.rewardsByHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AccountLevelShareBin> getAccountLevelShareBins() {
|
public List<AccountLevelShareBin> getAccountLevelShareBinsV1() {
|
||||||
return this.sharesByLevel;
|
return this.sharesByLevelV1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountLevelShareBin[] getShareBinsByAccountLevel() {
|
public List<AccountLevelShareBin> getAccountLevelShareBinsV2() {
|
||||||
return this.shareBinsByLevel;
|
return this.sharesByLevelV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountLevelShareBin[] getShareBinsByAccountLevelV1() {
|
||||||
|
return this.shareBinsByLevelV1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountLevelShareBin[] getShareBinsByAccountLevelV2() {
|
||||||
|
return this.shareBinsByLevelV2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Integer> getBlocksNeededByLevel() {
|
public List<Integer> getBlocksNeededByLevel() {
|
||||||
@ -345,14 +420,18 @@ public class BlockChain {
|
|||||||
return this.cumulativeBlocksByLevel;
|
return this.cumulativeBlocksByLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getQoraHoldersShare() {
|
|
||||||
return this.qoraHoldersShare;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getQoraPerQortReward() {
|
public long getQoraPerQortReward() {
|
||||||
return this.qoraPerQortReward;
|
return this.qoraPerQortReward;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMinAccountsToActivateShareBin() {
|
||||||
|
return this.minAccountsToActivateShareBin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getShareBinActivationMinLevel() {
|
||||||
|
return this.shareBinActivationMinLevel;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMinAccountLevelToMint() {
|
public int getMinAccountLevelToMint() {
|
||||||
return this.minAccountLevelToMint;
|
return this.minAccountLevelToMint;
|
||||||
}
|
}
|
||||||
@ -365,8 +444,8 @@ public class BlockChain {
|
|||||||
return this.minAccountLevelToRewardShare;
|
return this.minAccountLevelToRewardShare;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMaxRewardSharesPerMintingAccount() {
|
public int getMaxRewardSharesPerFounderMintingAccount() {
|
||||||
return this.maxRewardSharesPerMintingAccount;
|
return this.maxRewardSharesPerFounderMintingAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getFounderEffectiveMintingLevel() {
|
public int getFounderEffectiveMintingLevel() {
|
||||||
@ -399,6 +478,14 @@ public class BlockChain {
|
|||||||
return this.featureTriggers.get(FeatureTrigger.shareBinFix.name()).intValue();
|
return this.featureTriggers.get(FeatureTrigger.shareBinFix.name()).intValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSharesByLevelV2Height() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.sharesByLevelV2Height.name()).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRewardShareLimitTimestamp() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.rewardShareLimitTimestamp.name()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
public long getCalcChainWeightTimestamp() {
|
public long getCalcChainWeightTimestamp() {
|
||||||
return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue();
|
return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue();
|
||||||
}
|
}
|
||||||
@ -415,6 +502,27 @@ public class BlockChain {
|
|||||||
return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue();
|
return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getIncreaseOnlineAccountsDifficultyTimestamp() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.increaseOnlineAccountsDifficultyTimestamp.name()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelfSponsorshipAlgoV1Height() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV1Height.name()).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOnlineAccountMinterLevelValidationHeight() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.onlineAccountMinterLevelValidationHeight.name()).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFeeValidationFixTimestamp() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.feeValidationFixTimestamp.name()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getChatReferenceTimestamp() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// More complex getters for aspects that change by height or timestamp
|
// More complex getters for aspects that change by height or timestamp
|
||||||
|
|
||||||
public long getRewardAtHeight(int ourHeight) {
|
public long getRewardAtHeight(int ourHeight) {
|
||||||
@ -443,6 +551,23 @@ public class BlockChain {
|
|||||||
return this.getUnitFee();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getQoraHoldersShareAtHeight(int ourHeight) {
|
||||||
|
// Scan through for QORA share at our height
|
||||||
|
for (int i = qoraHoldersShareByHeight.size() - 1; i >= 0; --i)
|
||||||
|
if (qoraHoldersShareByHeight.get(i).height <= ourHeight)
|
||||||
|
return qoraHoldersShareByHeight.get(i).share;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/** Validate blockchain config read from JSON */
|
/** Validate blockchain config read from JSON */
|
||||||
private void validateConfig() {
|
private void validateConfig() {
|
||||||
if (this.genesisInfo == null)
|
if (this.genesisInfo == null)
|
||||||
@ -451,11 +576,14 @@ public class BlockChain {
|
|||||||
if (this.rewardsByHeight == null)
|
if (this.rewardsByHeight == null)
|
||||||
Settings.throwValidationError("No \"rewardsByHeight\" entry found in blockchain config");
|
Settings.throwValidationError("No \"rewardsByHeight\" entry found in blockchain config");
|
||||||
|
|
||||||
if (this.sharesByLevel == null)
|
if (this.sharesByLevelV1 == null)
|
||||||
Settings.throwValidationError("No \"sharesByLevel\" entry found in blockchain config");
|
Settings.throwValidationError("No \"sharesByLevelV1\" entry found in blockchain config");
|
||||||
|
|
||||||
if (this.qoraHoldersShare == null)
|
if (this.sharesByLevelV2 == null)
|
||||||
Settings.throwValidationError("No \"qoraHoldersShare\" entry found in blockchain config");
|
Settings.throwValidationError("No \"sharesByLevelV2\" entry found in blockchain config");
|
||||||
|
|
||||||
|
if (this.qoraHoldersShareByHeight == null)
|
||||||
|
Settings.throwValidationError("No \"qoraHoldersShareByHeight\" entry found in blockchain config");
|
||||||
|
|
||||||
if (this.qoraPerQortReward == null)
|
if (this.qoraPerQortReward == null)
|
||||||
Settings.throwValidationError("No \"qoraPerQortReward\" entry found in blockchain config");
|
Settings.throwValidationError("No \"qoraPerQortReward\" entry found in blockchain config");
|
||||||
@ -492,13 +620,22 @@ public class BlockChain {
|
|||||||
if (!this.featureTriggers.containsKey(featureTrigger.name()))
|
if (!this.featureTriggers.containsKey(featureTrigger.name()))
|
||||||
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
|
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
|
||||||
|
|
||||||
// Check block reward share bounds
|
// Check block reward share bounds (V1)
|
||||||
long totalShare = this.qoraHoldersShare;
|
long totalShareV1 = this.qoraHoldersShareByHeight.get(0).share;
|
||||||
// Add share percents for account-level-based rewards
|
// Add share percents for account-level-based rewards
|
||||||
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
|
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV1)
|
||||||
totalShare += accountLevelShareBin.share;
|
totalShareV1 += accountLevelShareBin.share;
|
||||||
|
|
||||||
if (totalShare < 0 || totalShare > 1_00000000L)
|
if (totalShareV1 < 0 || totalShareV1 > 1_00000000L)
|
||||||
|
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
|
||||||
|
|
||||||
|
// Check block reward share bounds (V2)
|
||||||
|
long totalShareV2 = this.qoraHoldersShareByHeight.get(1).share;
|
||||||
|
// Add share percents for account-level-based rewards
|
||||||
|
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV2)
|
||||||
|
totalShareV2 += accountLevelShareBin.share;
|
||||||
|
|
||||||
|
if (totalShareV2 < 0 || totalShareV2 > 1_00000000L)
|
||||||
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
|
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,23 +651,34 @@ public class BlockChain {
|
|||||||
cumulativeBlocks += this.blocksNeededByLevel.get(level);
|
cumulativeBlocks += this.blocksNeededByLevel.get(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate lookup-array for account-level share bins
|
// Generate lookup-array for account-level share bins (V1)
|
||||||
AccountLevelShareBin lastAccountLevelShareBin = this.sharesByLevel.get(this.sharesByLevel.size() - 1);
|
AccountLevelShareBin lastAccountLevelShareBinV1 = this.sharesByLevelV1.get(this.sharesByLevelV1.size() - 1);
|
||||||
final int lastLevel = lastAccountLevelShareBin.levels.get(lastAccountLevelShareBin.levels.size() - 1);
|
final int lastLevelV1 = lastAccountLevelShareBinV1.levels.get(lastAccountLevelShareBinV1.levels.size() - 1);
|
||||||
this.shareBinsByLevel = new AccountLevelShareBin[lastLevel];
|
this.shareBinsByLevelV1 = new AccountLevelShareBin[lastLevelV1];
|
||||||
|
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV1)
|
||||||
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
|
|
||||||
for (int level : accountLevelShareBin.levels)
|
for (int level : accountLevelShareBin.levels)
|
||||||
// level 1 stored at index 0, level 2 stored at index 1, etc.
|
// level 1 stored at index 0, level 2 stored at index 1, etc.
|
||||||
// level 0 not allowed
|
// level 0 not allowed
|
||||||
this.shareBinsByLevel[level - 1] = accountLevelShareBin;
|
this.shareBinsByLevelV1[level - 1] = accountLevelShareBin;
|
||||||
|
|
||||||
|
// Generate lookup-array for account-level share bins (V2)
|
||||||
|
AccountLevelShareBin lastAccountLevelShareBinV2 = this.sharesByLevelV2.get(this.sharesByLevelV2.size() - 1);
|
||||||
|
final int lastLevelV2 = lastAccountLevelShareBinV2.levels.get(lastAccountLevelShareBinV2.levels.size() - 1);
|
||||||
|
this.shareBinsByLevelV2 = new AccountLevelShareBin[lastLevelV2];
|
||||||
|
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV2)
|
||||||
|
for (int level : accountLevelShareBin.levels)
|
||||||
|
// level 1 stored at index 0, level 2 stored at index 1, etc.
|
||||||
|
// level 0 not allowed
|
||||||
|
this.shareBinsByLevelV2[level - 1] = accountLevelShareBin;
|
||||||
|
|
||||||
// Convert collections to unmodifiable form
|
// Convert collections to unmodifiable form
|
||||||
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
|
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
|
||||||
this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel);
|
this.sharesByLevelV1 = Collections.unmodifiableList(this.sharesByLevelV1);
|
||||||
|
this.sharesByLevelV2 = Collections.unmodifiableList(this.sharesByLevelV2);
|
||||||
this.blocksNeededByLevel = Collections.unmodifiableList(this.blocksNeededByLevel);
|
this.blocksNeededByLevel = Collections.unmodifiableList(this.blocksNeededByLevel);
|
||||||
this.cumulativeBlocksByLevel = Collections.unmodifiableList(this.cumulativeBlocksByLevel);
|
this.cumulativeBlocksByLevel = Collections.unmodifiableList(this.cumulativeBlocksByLevel);
|
||||||
this.blockTimingsByHeight = Collections.unmodifiableList(this.blockTimingsByHeight);
|
this.blockTimingsByHeight = Collections.unmodifiableList(this.blockTimingsByHeight);
|
||||||
|
this.qoraHoldersShareByHeight = Collections.unmodifiableList(this.qoraHoldersShareByHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -542,6 +690,7 @@ public class BlockChain {
|
|||||||
|
|
||||||
boolean isTopOnly = Settings.getInstance().isTopOnly();
|
boolean isTopOnly = Settings.getInstance().isTopOnly();
|
||||||
boolean archiveEnabled = Settings.getInstance().isArchiveEnabled();
|
boolean archiveEnabled = Settings.getInstance().isArchiveEnabled();
|
||||||
|
boolean isLite = Settings.getInstance().isLite();
|
||||||
boolean canBootstrap = Settings.getInstance().getBootstrap();
|
boolean canBootstrap = Settings.getInstance().getBootstrap();
|
||||||
boolean needsArchiveRebuild = false;
|
boolean needsArchiveRebuild = false;
|
||||||
BlockData chainTip;
|
BlockData chainTip;
|
||||||
@ -562,22 +711,44 @@ public class BlockChain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checkpoints
|
||||||
|
// Limited to topOnly nodes for now, in order to reduce risk, and to solve a real-world problem with divergent topOnly nodes
|
||||||
|
// TODO: remove the isTopOnly conditional below once this feature has had more testing time
|
||||||
|
if (isTopOnly && !isLite) {
|
||||||
|
List<Checkpoint> checkpoints = BlockChain.getInstance().getCheckpoints();
|
||||||
|
for (Checkpoint checkpoint : checkpoints) {
|
||||||
|
BlockData blockData = repository.getBlockRepository().fromHeight(checkpoint.height);
|
||||||
|
if (blockData == null) {
|
||||||
|
// Try the archive
|
||||||
|
blockData = repository.getBlockArchiveRepository().fromHeight(checkpoint.height);
|
||||||
|
}
|
||||||
|
if (blockData == null) {
|
||||||
|
LOGGER.trace("Couldn't find block for height {}", checkpoint.height);
|
||||||
|
// This is likely due to the block being pruned, so is safe to ignore.
|
||||||
|
// Continue, as there might be other blocks we can check more definitively.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] signature = Base58.decode(checkpoint.signature);
|
||||||
|
if (!Arrays.equals(signature, blockData.getSignature())) {
|
||||||
|
LOGGER.info("Error: block at height {} with signature {} doesn't match checkpoint sig: {}. Bootstrapping...", checkpoint.height, Base58.encode(blockData.getSignature()), checkpoint.signature);
|
||||||
|
needsArchiveRebuild = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
LOGGER.info("Block at height {} matches checkpoint signature", blockData.getHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1);
|
// Check first block is Genesis Block
|
||||||
|
if (!isGenesisBlockValid() || needsArchiveRebuild) {
|
||||||
|
try {
|
||||||
|
rebuildBlockchain();
|
||||||
|
|
||||||
if (isTopOnly && hasBlocks) {
|
} catch (InterruptedException e) {
|
||||||
// Top-only mode is enabled and we have blocks, so it's possible that the genesis block has been pruned
|
throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
|
||||||
// It's best not to validate it, and there's no real need to
|
|
||||||
} else {
|
|
||||||
// Check first block is Genesis Block
|
|
||||||
if (!isGenesisBlockValid() || needsArchiveRebuild) {
|
|
||||||
try {
|
|
||||||
rebuildBlockchain();
|
|
||||||
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,9 +757,7 @@ public class BlockChain {
|
|||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
repository.checkConsistency();
|
repository.checkConsistency();
|
||||||
|
|
||||||
// Set the number of blocks to validate based on the pruned state of the chain
|
int blocksToValidate = Math.min(Settings.getInstance().getPruneBlockLimit() - 10, 1440);
|
||||||
// If pruned, subtract an extra 10 to allow room for error
|
|
||||||
int blocksToValidate = (isTopOnly || archiveEnabled) ? Settings.getInstance().getPruneBlockLimit() - 10 : 1440;
|
|
||||||
|
|
||||||
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - blocksToValidate, 1);
|
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - blocksToValidate, 1);
|
||||||
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
|
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
|
||||||
|
133
src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
Normal file
133
src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package org.qortal.block;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.account.SelfSponsorshipAlgoV1;
|
||||||
|
import org.qortal.api.model.AccountPenaltyStats;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.account.AccountPenaltyData;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Self Sponsorship AlgoV1 Block
|
||||||
|
* <p>
|
||||||
|
* Selected block for the initial run on the "self sponsorship detection algorithm"
|
||||||
|
*/
|
||||||
|
public final class SelfSponsorshipAlgoV1Block {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV1Block.class);
|
||||||
|
|
||||||
|
|
||||||
|
private SelfSponsorshipAlgoV1Block() {
|
||||||
|
/* Do not instantiate */
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void processAccountPenalties(Block block) throws DataException {
|
||||||
|
LOGGER.info("Running algo for block processing - this will take a while...");
|
||||||
|
logPenaltyStats(block.repository);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, -5000000);
|
||||||
|
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||||
|
long totalTime = System.currentTimeMillis() - startTime;
|
||||||
|
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||||
|
LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||||
|
logPenaltyStats(block.repository);
|
||||||
|
|
||||||
|
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||||
|
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void orphanAccountPenalties(Block block) throws DataException {
|
||||||
|
LOGGER.info("Running algo for block orphaning - this will take a while...");
|
||||||
|
logPenaltyStats(block.repository);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, 5000000);
|
||||||
|
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||||
|
long totalTime = System.currentTimeMillis() - startTime;
|
||||||
|
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||||
|
LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||||
|
logPenaltyStats(block.repository);
|
||||||
|
|
||||||
|
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||||
|
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<AccountPenaltyData> getAccountPenalties(Repository repository, int penalty) throws DataException {
|
||||||
|
final long snapshotTimestamp = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
|
||||||
|
Set<AccountPenaltyData> penalties = new LinkedHashSet<>();
|
||||||
|
List<String> addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares();
|
||||||
|
for (String address : addresses) {
|
||||||
|
//System.out.println(String.format("address: %s", address));
|
||||||
|
SelfSponsorshipAlgoV1 selfSponsorshipAlgoV1 = new SelfSponsorshipAlgoV1(repository, address, snapshotTimestamp, false);
|
||||||
|
selfSponsorshipAlgoV1.run();
|
||||||
|
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV1.getPenaltyAddresses().size()));
|
||||||
|
|
||||||
|
for (String penaltyAddress : selfSponsorshipAlgoV1.getPenaltyAddresses()) {
|
||||||
|
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return penalties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||||
|
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||||
|
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||||
|
|
||||||
|
int updatedCount = 0;
|
||||||
|
|
||||||
|
for (AccountPenaltyData penaltyData : accountPenalties) {
|
||||||
|
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
|
||||||
|
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||||
|
|
||||||
|
// Shortcut for penalties
|
||||||
|
if (effectiveBlocksMinted < 0) {
|
||||||
|
accountData.setLevel(0);
|
||||||
|
repository.getAccountRepository().setLevel(accountData);
|
||||||
|
updatedCount++;
|
||||||
|
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
|
||||||
|
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||||
|
accountData.setLevel(newLevel);
|
||||||
|
repository.getAccountRepository().setLevel(accountData);
|
||||||
|
updatedCount++;
|
||||||
|
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void logPenaltyStats(Repository repository) {
|
||||||
|
try {
|
||||||
|
LOGGER.info(getPenaltyStats(repository));
|
||||||
|
|
||||||
|
} catch (DataException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
|
||||||
|
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||||
|
return AccountPenaltyStats.fromAccounts(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getHash(List<String> penaltyAddresses) {
|
||||||
|
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Collections.sort(penaltyAddresses);
|
||||||
|
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -40,6 +41,7 @@ public class AutoUpdate extends Thread {
|
|||||||
|
|
||||||
public static final String JAR_FILENAME = "qortal.jar";
|
public static final String JAR_FILENAME = "qortal.jar";
|
||||||
public static final String NEW_JAR_FILENAME = "new-" + JAR_FILENAME;
|
public static final String NEW_JAR_FILENAME = "new-" + JAR_FILENAME;
|
||||||
|
public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib=";
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(AutoUpdate.class);
|
private static final Logger LOGGER = LogManager.getLogger(AutoUpdate.class);
|
||||||
private static final long CHECK_INTERVAL = 20 * 60 * 1000L; // ms
|
private static final long CHECK_INTERVAL = 20 * 60 * 1000L; // ms
|
||||||
@ -243,6 +245,11 @@ public class AutoUpdate extends Thread {
|
|||||||
// JVM arguments
|
// JVM arguments
|
||||||
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||||
|
|
||||||
|
// Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
|
||||||
|
javaCmd = javaCmd.stream()
|
||||||
|
.map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// Remove JNI options as they won't be supported by command-line 'java'
|
// Remove JNI options as they won't be supported by command-line 'java'
|
||||||
// These are typically added by the AdvancedInstaller Java launcher EXE
|
// These are typically added by the AdvancedInstaller Java launcher EXE
|
||||||
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
|
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
|
||||||
@ -261,10 +268,19 @@ public class AutoUpdate extends Thread {
|
|||||||
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"),
|
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"),
|
||||||
MessageType.INFO);
|
MessageType.INFO);
|
||||||
|
|
||||||
new ProcessBuilder(javaCmd).start();
|
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||||
|
|
||||||
|
// New process will inherit our stdout and stderr
|
||||||
|
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
||||||
|
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
|
||||||
|
|
||||||
|
Process process = processBuilder.start();
|
||||||
|
|
||||||
|
// Nothing to pipe to new process, so close output stream (process's stdin)
|
||||||
|
process.getOutputStream().close();
|
||||||
|
|
||||||
return true; // applying update OK
|
return true; // applying update OK
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error(String.format("Failed to apply update: %s", e.getMessage()));
|
LOGGER.error(String.format("Failed to apply update: %s", e.getMessage()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -35,6 +35,8 @@ import org.qortal.transaction.Transaction;
|
|||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
// Minting new blocks
|
// Minting new blocks
|
||||||
|
|
||||||
public class BlockMinter extends Thread {
|
public class BlockMinter extends Thread {
|
||||||
@ -61,13 +63,12 @@ public class BlockMinter extends Thread {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("BlockMinter");
|
Thread.currentThread().setName("BlockMinter");
|
||||||
|
|
||||||
if (Settings.getInstance().isLite()) {
|
if (Settings.getInstance().isTopOnly() || Settings.getInstance().isLite()) {
|
||||||
// Lite nodes do not mint
|
// Top only and lite nodes do not sign blocks
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
|
||||||
// Wipe existing unconfirmed transactions
|
// Wipe existing unconfirmed transactions
|
||||||
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||||
|
|
||||||
@ -77,356 +78,377 @@ public class BlockMinter extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.warn("Repository issue trying to wipe unconfirmed transactions on start-up: {}", e.getMessage());
|
||||||
|
// Fall-through to normal behaviour in case we can recover
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockData previousBlockData = null;
|
||||||
|
|
||||||
|
// Vars to keep track of blocks that were skipped due to chain weight
|
||||||
|
byte[] parentSignatureForLastLowWeightBlock = null;
|
||||||
|
Long timeOfLastLowWeightBlock = null;
|
||||||
|
|
||||||
|
List<Block> newBlocks = new ArrayList<>();
|
||||||
|
|
||||||
|
final boolean isSingleNodeTestnet = Settings.getInstance().isSingleNodeTestnet();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
// Going to need this a lot...
|
// Going to need this a lot...
|
||||||
BlockRepository blockRepository = repository.getBlockRepository();
|
BlockRepository blockRepository = repository.getBlockRepository();
|
||||||
BlockData previousBlockData = null;
|
|
||||||
|
|
||||||
// Vars to keep track of blocks that were skipped due to chain weight
|
|
||||||
byte[] parentSignatureForLastLowWeightBlock = null;
|
|
||||||
Long timeOfLastLowWeightBlock = null;
|
|
||||||
|
|
||||||
List<Block> newBlocks = new ArrayList<>();
|
|
||||||
|
|
||||||
// Flags for tracking change in whether minting is possible,
|
// Flags for tracking change in whether minting is possible,
|
||||||
// so we can notify Controller, and further update SysTray, etc.
|
// so we can notify Controller, and further update SysTray, etc.
|
||||||
boolean isMintingPossible = false;
|
boolean isMintingPossible = false;
|
||||||
boolean wasMintingPossible = isMintingPossible;
|
boolean wasMintingPossible = isMintingPossible;
|
||||||
while (running) {
|
while (running) {
|
||||||
repository.discardChanges(); // Free repository locks, if any
|
|
||||||
|
|
||||||
if (isMintingPossible != wasMintingPossible)
|
if (isMintingPossible != wasMintingPossible)
|
||||||
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
|
||||||
|
|
||||||
wasMintingPossible = isMintingPossible;
|
wasMintingPossible = isMintingPossible;
|
||||||
|
|
||||||
// Sleep for a while
|
|
||||||
Thread.sleep(1000);
|
|
||||||
|
|
||||||
isMintingPossible = false;
|
|
||||||
|
|
||||||
final Long now = NTP.getTime();
|
|
||||||
if (now == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
|
||||||
if (minLatestBlockTimestamp == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// No online accounts? (e.g. during startup)
|
|
||||||
if (OnlineAccountsManager.getInstance().getOnlineAccounts().isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
|
||||||
// No minting accounts?
|
|
||||||
if (mintingAccountsData.isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
|
|
||||||
// Note that minting accounts are actually reward-shares in Qortal
|
|
||||||
Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
|
|
||||||
while (madi.hasNext()) {
|
|
||||||
MintingAccountData mintingAccountData = madi.next();
|
|
||||||
|
|
||||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
|
||||||
if (rewardShareData == null) {
|
|
||||||
// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
|
|
||||||
madi.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
|
||||||
if (!mintingAccount.canMint()) {
|
|
||||||
// Minting-account component of reward-share can no longer mint - disregard
|
|
||||||
madi.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional (non-validated) prevention of block submissions below a defined level.
|
|
||||||
// This is an unvalidated version of Blockchain.minAccountLevelToMint
|
|
||||||
// and exists only to reduce block candidates by default.
|
|
||||||
int level = mintingAccount.getEffectiveMintingLevel();
|
|
||||||
if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
|
|
||||||
madi.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Needs a mutable copy of the unmodifiableList
|
|
||||||
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
|
||||||
BlockData lastBlockData = blockRepository.getLastBlock();
|
|
||||||
|
|
||||||
// Disregard peers that have "misbehaved" recently
|
|
||||||
peers.removeIf(Controller.hasMisbehaved);
|
|
||||||
|
|
||||||
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
|
||||||
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
|
|
||||||
if (Synchronizer.getInstance().getRecoveryMode() == false)
|
|
||||||
peers.removeIf(Controller.hasNoRecentBlock);
|
|
||||||
|
|
||||||
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
|
||||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// If we are stuck on an invalid block, we should allow an alternative to be minted
|
|
||||||
boolean recoverInvalidBlock = false;
|
|
||||||
if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
|
|
||||||
// We've had at least one invalid block
|
|
||||||
long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
|
|
||||||
long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
|
|
||||||
if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
|
||||||
if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
|
||||||
// Last valid block was more than 10 mins ago, but we've had an invalid block since then
|
|
||||||
// Assume that the chain has stalled because there is no alternative valid candidate
|
|
||||||
// Enter recovery mode to allow alternative, valid candidates to be minted
|
|
||||||
recoverInvalidBlock = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
|
||||||
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
|
||||||
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// There are enough peers with a recent block and our latest block is recent
|
|
||||||
// so go ahead and mint a block if possible.
|
|
||||||
isMintingPossible = true;
|
|
||||||
|
|
||||||
// Check blockchain hasn't changed
|
|
||||||
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
|
|
||||||
previousBlockData = lastBlockData;
|
|
||||||
newBlocks.clear();
|
|
||||||
|
|
||||||
// Reduce log timeout
|
|
||||||
logTimeout = 10 * 1000L;
|
|
||||||
|
|
||||||
// Last low weight block is no longer valid
|
|
||||||
parentSignatureForLastLowWeightBlock = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard accounts we have already built blocks with
|
|
||||||
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
|
|
||||||
|
|
||||||
// Do we need to build any potential new blocks?
|
|
||||||
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 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"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
newBlocks.add(newBlock);
|
|
||||||
} else {
|
|
||||||
// The blocks for other minters require less effort...
|
|
||||||
Block newBlock = newBlocks.get(0).remint(mintingAccount);
|
|
||||||
if (newBlock == null) {
|
|
||||||
// For some reason we can't mint right now
|
|
||||||
moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
newBlocks.add(newBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No potential block candidates?
|
|
||||||
if (newBlocks.isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Make sure we're the only thread modifying the blockchain
|
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
|
||||||
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
|
|
||||||
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean newBlockMinted = false;
|
|
||||||
Block newBlock = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clear repository session state so we have latest view of data
|
// Free up any repository locks
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
|
|
||||||
// Now that we have blockchain lock, do final check that chain hasn't changed
|
// Sleep for a while.
|
||||||
BlockData latestBlockData = blockRepository.getLastBlock();
|
// It's faster on single node testnets, to allow lots of blocks to be minted quickly.
|
||||||
if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
|
Thread.sleep(isSingleNodeTestnet ? 50 : 1000);
|
||||||
|
|
||||||
|
isMintingPossible = false;
|
||||||
|
|
||||||
|
final Long now = NTP.getTime();
|
||||||
|
if (now == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
List<Block> goodBlocks = new ArrayList<>();
|
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||||
for (Block testBlock : newBlocks) {
|
if (minLatestBlockTimestamp == null)
|
||||||
// Is new block's timestamp valid yet?
|
continue;
|
||||||
// We do a separate check as some timestamp checks are skipped for testchains
|
|
||||||
if (testBlock.isTimestampValid() != ValidationResult.OK)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
testBlock.preProcess();
|
// No online accounts for current timestamp? (e.g. during startup)
|
||||||
|
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
|
||||||
|
continue;
|
||||||
|
|
||||||
// Is new block valid yet? (Before adding unconfirmed transactions)
|
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
||||||
ValidationResult result = testBlock.isValid();
|
// No minting accounts?
|
||||||
if (result != ValidationResult.OK) {
|
if (mintingAccountsData.isEmpty())
|
||||||
moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
|
continue;
|
||||||
|
|
||||||
|
// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
|
||||||
|
// Note that minting accounts are actually reward-shares in Qortal
|
||||||
|
Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
|
||||||
|
while (madi.hasNext()) {
|
||||||
|
MintingAccountData mintingAccountData = madi.next();
|
||||||
|
|
||||||
|
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
||||||
|
if (rewardShareData == null) {
|
||||||
|
// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
|
||||||
|
madi.remove();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
goodBlocks.add(testBlock);
|
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||||
}
|
if (!mintingAccount.canMint()) {
|
||||||
|
// Minting-account component of reward-share can no longer mint - disregard
|
||||||
|
madi.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (goodBlocks.isEmpty())
|
// Optional (non-validated) prevention of block submissions below a defined level.
|
||||||
continue;
|
// This is an unvalidated version of Blockchain.minAccountLevelToMint
|
||||||
|
// and exists only to reduce block candidates by default.
|
||||||
// Pick best block
|
int level = mintingAccount.getEffectiveMintingLevel();
|
||||||
final int parentHeight = previousBlockData.getHeight();
|
if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
|
||||||
final byte[] parentBlockSignature = previousBlockData.getSignature();
|
madi.remove();
|
||||||
|
continue;
|
||||||
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 {
|
// Needs a mutable copy of the unmodifiableList
|
||||||
if (this.higherWeightChainExists(repository, bestWeight)) {
|
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||||
|
BlockData lastBlockData = blockRepository.getLastBlock();
|
||||||
|
|
||||||
// Check if the base block has updated since the last time we were here
|
// Disregard peers that have "misbehaved" recently
|
||||||
if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
|
peers.removeIf(Controller.hasMisbehaved);
|
||||||
!Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
|
|
||||||
// We've switched to a different chain, so reset the timer
|
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
|
||||||
timeOfLastLowWeightBlock = NTP.getTime();
|
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
|
||||||
|
if (Synchronizer.getInstance().getRecoveryMode() == false)
|
||||||
|
peers.removeIf(Controller.hasNoRecentBlock);
|
||||||
|
|
||||||
|
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
|
||||||
|
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// If we are stuck on an invalid block, we should allow an alternative to be minted
|
||||||
|
boolean recoverInvalidBlock = false;
|
||||||
|
if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
|
||||||
|
// We've had at least one invalid block
|
||||||
|
long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
|
||||||
|
long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
|
||||||
|
if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
||||||
|
if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
|
||||||
|
// Last valid block was more than 10 mins ago, but we've had an invalid block since then
|
||||||
|
// Assume that the chain has stalled because there is no alternative valid candidate
|
||||||
|
// Enter recovery mode to allow alternative, valid candidates to be minted
|
||||||
|
recoverInvalidBlock = true;
|
||||||
}
|
}
|
||||||
parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If less than 30 seconds has passed since first detection the higher weight chain,
|
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
|
||||||
// we should skip our block submission to give us the opportunity to sync to the better chain
|
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||||
if (NTP.getTime() - timeOfLastLowWeightBlock < 30*1000L) {
|
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
||||||
LOGGER.debug("Higher weight chain found in peers, so not signing a block this round");
|
continue;
|
||||||
LOGGER.debug("Time since detected: {}ms", NTP.getTime() - timeOfLastLowWeightBlock);
|
|
||||||
|
// There are enough peers with a recent block and our latest block is recent
|
||||||
|
// so go ahead and mint a block if possible.
|
||||||
|
isMintingPossible = true;
|
||||||
|
|
||||||
|
// Check blockchain hasn't changed
|
||||||
|
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
|
||||||
|
previousBlockData = lastBlockData;
|
||||||
|
newBlocks.clear();
|
||||||
|
|
||||||
|
// Reduce log timeout
|
||||||
|
logTimeout = 10 * 1000L;
|
||||||
|
|
||||||
|
// Last low weight block is no longer valid
|
||||||
|
parentSignatureForLastLowWeightBlock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard accounts we have already built blocks with
|
||||||
|
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
|
||||||
|
|
||||||
|
// Do we need to build any potential new blocks?
|
||||||
|
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// We might need to sit the next block out, if one of our minting accounts signed the previous one
|
||||||
|
// Skip this check for single node testnets, since they definitely need to mint every block
|
||||||
|
byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
|
||||||
|
boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
|
||||||
|
if (mintedLastBlock && !isSingleNodeTestnet) {
|
||||||
|
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.info("Couldn't build a to-be-minted block"));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// More than 30 seconds have passed, so we should submit our block candidate anyway.
|
newBlocks.add(newBlock);
|
||||||
LOGGER.debug("More than 30 seconds passed, so proceeding to submit block candidate...");
|
} 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
|
// No potential block candidates?
|
||||||
repository.discardChanges();
|
if (newBlocks.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
// Clear variables that track low weight blocks
|
// Make sure we're the only thread modifying the blockchain
|
||||||
parentSignatureForLastLowWeightBlock = null;
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
timeOfLastLowWeightBlock = null;
|
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
|
||||||
|
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
|
||||||
|
|
||||||
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to blockchain - something else will notice and broadcast new block to network
|
boolean newBlockMinted = false;
|
||||||
|
Block newBlock = null;
|
||||||
|
|
||||||
try {
|
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) {
|
// Is new block's timestamp valid yet?
|
||||||
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
|
// We do a separate check as some timestamp checks are skipped for testchains
|
||||||
newBlock.getBlockData().getHeight(),
|
if (testBlock.isTimestampValid() != ValidationResult.OK)
|
||||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
continue;
|
||||||
Base58.encode(newBlock.getParent().getSignature()),
|
|
||||||
rewardShareData.getMinter(),
|
testBlock.preProcess();
|
||||||
rewardShareData.getRecipient()));
|
|
||||||
} else {
|
// Is new block valid yet? (Before adding unconfirmed transactions)
|
||||||
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
|
ValidationResult result = testBlock.isValid();
|
||||||
newBlock.getBlockData().getHeight(),
|
if (result != ValidationResult.OK) {
|
||||||
Base58.encode(newBlock.getBlockData().getSignature()),
|
moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
|
||||||
Base58.encode(newBlock.getParent().getSignature()),
|
|
||||||
newBlock.getMinter().getAddress()));
|
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
|
if (wasInvalidBlockDiscarded || goodBlocks.isEmpty())
|
||||||
newBlockMinted = true;
|
continue;
|
||||||
|
|
||||||
// Notify Controller
|
// Pick best block
|
||||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
final int parentHeight = previousBlockData.getHeight();
|
||||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
final byte[] parentBlockSignature = previousBlockData.getSignature();
|
||||||
} catch (DataException e) {
|
|
||||||
// Unable to process block - report and discard
|
BigInteger bestWeight = null;
|
||||||
LOGGER.error("Unable to process newly minted block?", e);
|
|
||||||
newBlocks.clear();
|
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) {
|
if (newBlockMinted) {
|
||||||
// Broadcast our new chain to network
|
// Broadcast our new chain to network
|
||||||
BlockData newBlockData = newBlock.getBlockData();
|
Network.getInstance().broadcastOurChain();
|
||||||
|
}
|
||||||
|
|
||||||
Network network = Network.getInstance();
|
} catch (InterruptedException e) {
|
||||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
|
// We've been interrupted - time to exit
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
LOGGER.warn("Repository issue while running block minter", e);
|
LOGGER.warn("Repository issue while running block minter - NO LONGER MINTING", e);
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// We've been interrupted - time to exit
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,6 +510,21 @@ public class BlockMinter extends Thread {
|
|||||||
|
|
||||||
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||||
|
|
||||||
|
Block block = mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||||
|
assertNotNull("Minted block must not be null", block);
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Block mintTestingBlockUnvalidated(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
|
||||||
|
if (!BlockChain.getInstance().isTestChain())
|
||||||
|
throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
|
||||||
|
|
||||||
|
// Ensure mintingAccount is 'online' so blocks can be minted
|
||||||
|
OnlineAccountsManager.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
|
||||||
|
|
||||||
|
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||||
|
|
||||||
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,6 +532,8 @@ public class BlockMinter extends Thread {
|
|||||||
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
|
|
||||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||||
|
if (newBlock == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
// Make sure we're the only thread modifying the blockchain
|
// Make sure we're the only thread modifying the blockchain
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
@ -557,18 +596,23 @@ public class BlockMinter extends Thread {
|
|||||||
// This peer has common block data
|
// This peer has common block data
|
||||||
CommonBlockData commonBlockData = peer.getCommonBlockData();
|
CommonBlockData commonBlockData = peer.getCommonBlockData();
|
||||||
BlockSummaryData commonBlockSummaryData = commonBlockData.getCommonBlockSummary();
|
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
|
// The synchronizer has calculated this peer's chain weight
|
||||||
BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
|
if (!Synchronizer.getInstance().containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
|
||||||
BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
|
// .. and it doesn't hold any invalid blocks
|
||||||
BigInteger peerChainWeight = commonBlockData.getChainWeight();
|
BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
|
||||||
if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
|
BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
|
||||||
// This peer has a higher weight chain than ours
|
BigInteger peerChainWeight = commonBlockData.getChainWeight();
|
||||||
LOGGER.debug("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
|
if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
|
||||||
return true;
|
// 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 {
|
} 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 {
|
} else {
|
||||||
LOGGER.debug("Peer {} has no chain weight", peer);
|
LOGGER.debug("Peer {} has no chain weight", peer);
|
||||||
|
@ -29,6 +29,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||||
|
import org.qortal.account.Account;
|
||||||
import org.qortal.api.ApiService;
|
import org.qortal.api.ApiService;
|
||||||
import org.qortal.api.DomainMapService;
|
import org.qortal.api.DomainMapService;
|
||||||
import org.qortal.api.GatewayService;
|
import org.qortal.api.GatewayService;
|
||||||
@ -45,7 +46,6 @@ import org.qortal.data.account.AccountData;
|
|||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.data.block.BlockSummaryData;
|
import org.qortal.data.block.BlockSummaryData;
|
||||||
import org.qortal.data.naming.NameData;
|
import org.qortal.data.naming.NameData;
|
||||||
import org.qortal.data.network.PeerChainTipData;
|
|
||||||
import org.qortal.data.network.PeerData;
|
import org.qortal.data.network.PeerData;
|
||||||
import org.qortal.data.transaction.ChatTransactionData;
|
import org.qortal.data.transaction.ChatTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
@ -113,6 +113,7 @@ public class Controller extends Thread {
|
|||||||
private long repositoryBackupTimestamp = startTime; // ms
|
private long repositoryBackupTimestamp = startTime; // ms
|
||||||
private long repositoryMaintenanceTimestamp = startTime; // ms
|
private long repositoryMaintenanceTimestamp = startTime; // ms
|
||||||
private long repositoryCheckpointTimestamp = startTime; // ms
|
private long repositoryCheckpointTimestamp = startTime; // ms
|
||||||
|
private long prunePeersTimestamp = startTime; // ms
|
||||||
private long ntpCheckTimestamp = startTime; // ms
|
private long ntpCheckTimestamp = startTime; // ms
|
||||||
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
|
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
|
||||||
|
|
||||||
@ -316,6 +317,10 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long uptime() {
|
||||||
|
return System.currentTimeMillis() - Controller.startTime;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns highest block, or null if it's not available. */
|
/** Returns highest block, or null if it's not available. */
|
||||||
public BlockData getChainTip() {
|
public BlockData getChainTip() {
|
||||||
synchronized (this.latestBlocks) {
|
synchronized (this.latestBlocks) {
|
||||||
@ -496,6 +501,9 @@ public class Controller extends Thread {
|
|||||||
AutoUpdate.getInstance().start();
|
AutoUpdate.getInstance().start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Starting wallets");
|
||||||
|
PirateChainWalletController.getInstance().start();
|
||||||
|
|
||||||
LOGGER.info(String.format("Starting API on port %d", Settings.getInstance().getApiPort()));
|
LOGGER.info(String.format("Starting API on port %d", Settings.getInstance().getApiPort()));
|
||||||
try {
|
try {
|
||||||
ApiService apiService = ApiService.getInstance();
|
ApiService apiService = ApiService.getInstance();
|
||||||
@ -552,6 +560,7 @@ public class Controller extends Thread {
|
|||||||
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
||||||
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
|
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
|
||||||
long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval();
|
long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval();
|
||||||
|
final long prunePeersInterval = 5 * 60 * 1000L; // Every 5 minutes
|
||||||
|
|
||||||
// Start executor service for trimming or pruning
|
// Start executor service for trimming or pruning
|
||||||
PruneManager.getInstance().start();
|
PruneManager.getInstance().start();
|
||||||
@ -649,10 +658,15 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prune stuck/slow/old peers
|
// Prune stuck/slow/old peers
|
||||||
try {
|
if (now >= prunePeersTimestamp + prunePeersInterval) {
|
||||||
Network.getInstance().prunePeers();
|
prunePeersTimestamp = now + prunePeersInterval;
|
||||||
} catch (DataException e) {
|
|
||||||
LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
|
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
|
// Delete expired transactions
|
||||||
@ -717,25 +731,25 @@ public class Controller extends Thread {
|
|||||||
|
|
||||||
public static final Predicate<Peer> hasNoRecentBlock = peer -> {
|
public static final Predicate<Peer> hasNoRecentBlock = peer -> {
|
||||||
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
|
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
|
||||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
return peerChainTipData == null || peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp;
|
return peerChainTipData == null || peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp;
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Predicate<Peer> hasNoOrSameBlock = peer -> {
|
public static final Predicate<Peer> hasNoOrSameBlock = peer -> {
|
||||||
final BlockData latestBlockData = getInstance().getChainTip();
|
final BlockData latestBlockData = getInstance().getChainTip();
|
||||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getLastBlockSignature());
|
return peerChainTipData == null || peerChainTipData.getSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getSignature());
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Predicate<Peer> hasOnlyGenesisBlock = peer -> {
|
public static final Predicate<Peer> hasOnlyGenesisBlock = peer -> {
|
||||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
return peerChainTipData == null || peerChainTipData.getLastHeight() == null || peerChainTipData.getLastHeight() == 1;
|
return peerChainTipData == null || peerChainTipData.getHeight() == 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Predicate<Peer> hasInferiorChainTip = peer -> {
|
public static final Predicate<Peer> hasInferiorChainTip = peer -> {
|
||||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
final List<ByteArray> inferiorChainTips = Synchronizer.getInstance().inferiorChainSignatures;
|
final List<ByteArray> inferiorChainTips = Synchronizer.getInstance().inferiorChainSignatures;
|
||||||
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getLastBlockSignature()));
|
return peerChainTipData == null || peerChainTipData.getSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getSignature()));
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Predicate<Peer> hasOldVersion = peer -> {
|
public static final Predicate<Peer> hasOldVersion = peer -> {
|
||||||
@ -743,6 +757,28 @@ public class Controller extends Thread {
|
|||||||
return peer.isAtLeastVersion(minPeerVersion) == false;
|
return peer.isAtLeastVersion(minPeerVersion) == false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Predicate<Peer> hasInvalidSigner = peer -> {
|
||||||
|
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
|
if (peerChainTipData == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
try (Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
return Account.getRewardShareEffectiveMintingLevel(repository, peerChainTipData.getMinterPublicKey()) == 0;
|
||||||
|
} catch (DataException e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Predicate<Peer> wasRecentlyTooDivergent = peer -> {
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
Long peerLastTooDivergentTime = peer.getLastTooDivergentTime();
|
||||||
|
if (now == null || peerLastTooDivergentTime == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Exclude any peers that were TOO_DIVERGENT in the last 5 mins
|
||||||
|
return (now - peerLastTooDivergentTime < 5 * 60 * 1000L);
|
||||||
|
};
|
||||||
|
|
||||||
private long getRandomRepositoryMaintenanceInterval() {
|
private long getRandomRepositoryMaintenanceInterval() {
|
||||||
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
|
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
|
||||||
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
|
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
|
||||||
@ -812,7 +848,7 @@ public class Controller extends Thread {
|
|||||||
actionText = String.format("%s", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"));
|
actionText = String.format("%s", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"));
|
||||||
SysTray.getInstance().setTrayIcon(3);
|
SysTray.getInstance().setTrayIcon(3);
|
||||||
}
|
}
|
||||||
else if (OnlineAccountsManager.getInstance().hasOnlineAccounts()) {
|
else if (OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures()) {
|
||||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||||
SysTray.getInstance().setTrayIcon(2);
|
SysTray.getInstance().setTrayIcon(2);
|
||||||
}
|
}
|
||||||
@ -825,6 +861,12 @@ public class Controller extends Thread {
|
|||||||
String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText);
|
String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText);
|
||||||
if (!Settings.getInstance().isLite()) {
|
if (!Settings.getInstance().isLite()) {
|
||||||
tooltip = tooltip.concat(String.format(" - %s %d", heightText, height));
|
tooltip = tooltip.concat(String.format(" - %s %d", heightText, height));
|
||||||
|
|
||||||
|
final Integer blocksRemaining = Synchronizer.getInstance().getBlocksRemaining();
|
||||||
|
if (blocksRemaining != null && blocksRemaining > 0) {
|
||||||
|
String blocksRemainingText = Translator.INSTANCE.translate("SysTray", "BLOCKS_REMAINING");
|
||||||
|
tooltip = tooltip.concat(String.format(" - %d %s", blocksRemaining, blocksRemainingText));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tooltip = tooltip.concat(String.format("\n%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion));
|
tooltip = tooltip.concat(String.format("\n%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion));
|
||||||
SysTray.getInstance().setToolTipText(tooltip);
|
SysTray.getInstance().setToolTipText(tooltip);
|
||||||
@ -883,6 +925,9 @@ public class Controller extends Thread {
|
|||||||
LOGGER.info("Shutting down API");
|
LOGGER.info("Shutting down API");
|
||||||
ApiService.getInstance().stop();
|
ApiService.getInstance().stop();
|
||||||
|
|
||||||
|
LOGGER.info("Shutting down wallets");
|
||||||
|
PirateChainWalletController.getInstance().shutdown();
|
||||||
|
|
||||||
if (Settings.getInstance().isAutoUpdateEnabled()) {
|
if (Settings.getInstance().isAutoUpdateEnabled()) {
|
||||||
LOGGER.info("Shutting down auto-update");
|
LOGGER.info("Shutting down auto-update");
|
||||||
AutoUpdate.getInstance().shutdown();
|
AutoUpdate.getInstance().shutdown();
|
||||||
@ -994,8 +1039,7 @@ public class Controller extends Thread {
|
|||||||
network.broadcast(peer -> peer.isOutbound() ? network.buildPeersMessage(peer) : new GetPeersMessage());
|
network.broadcast(peer -> peer.isOutbound() ? network.buildPeersMessage(peer) : new GetPeersMessage());
|
||||||
|
|
||||||
// Send our current height
|
// Send our current height
|
||||||
BlockData latestBlockData = getChainTip();
|
network.broadcastOurChain();
|
||||||
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
|
|
||||||
|
|
||||||
// Request unconfirmed transaction signatures, but only if we're up-to-date.
|
// Request unconfirmed transaction signatures, but only if we're up-to-date.
|
||||||
// If we're NOT up-to-date then priority is synchronizing first
|
// If we're NOT up-to-date then priority is synchronizing first
|
||||||
@ -1202,6 +1246,10 @@ public class Controller extends Thread {
|
|||||||
onNetworkHeightV2Message(peer, message);
|
onNetworkHeightV2Message(peer, message);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case BLOCK_SUMMARIES_V2:
|
||||||
|
onNetworkBlockSummariesV2Message(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
case GET_TRANSACTION:
|
case GET_TRANSACTION:
|
||||||
TransactionImporter.getInstance().onNetworkGetTransactionMessage(peer, message);
|
TransactionImporter.getInstance().onNetworkGetTransactionMessage(peer, message);
|
||||||
break;
|
break;
|
||||||
@ -1219,19 +1267,18 @@ public class Controller extends Thread {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case GET_ONLINE_ACCOUNTS:
|
case GET_ONLINE_ACCOUNTS:
|
||||||
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsMessage(peer, message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ONLINE_ACCOUNTS:
|
case ONLINE_ACCOUNTS:
|
||||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsMessage(peer, message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case GET_ONLINE_ACCOUNTS_V2:
|
case GET_ONLINE_ACCOUNTS_V2:
|
||||||
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV2Message(peer, message);
|
case ONLINE_ACCOUNTS_V2:
|
||||||
|
// No longer supported - to be eventually removed
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ONLINE_ACCOUNTS_V2:
|
case GET_ONLINE_ACCOUNTS_V3:
|
||||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV2Message(peer, message);
|
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ONLINE_ACCOUNTS_V3:
|
||||||
|
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GET_ARBITRARY_DATA:
|
case GET_ARBITRARY_DATA:
|
||||||
@ -1357,8 +1404,10 @@ public class Controller extends Thread {
|
|||||||
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
|
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
|
||||||
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
|
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
|
||||||
|
|
||||||
// We'll send empty block summaries message as it's very short
|
// Send generic 'unknown' message as it's very short
|
||||||
Message blockUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
Message blockUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
|
||||||
|
? new GenericUnknownMessage()
|
||||||
|
: new BlockSummariesMessage(Collections.emptyList());
|
||||||
blockUnknownMessage.setId(message.getId());
|
blockUnknownMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(blockUnknownMessage))
|
if (!peer.sendMessage(blockUnknownMessage))
|
||||||
peer.disconnect("failed to send block-unknown response");
|
peer.disconnect("failed to send block-unknown response");
|
||||||
@ -1407,11 +1456,15 @@ public class Controller extends Thread {
|
|||||||
this.stats.getBlockSummariesStats.requests.incrementAndGet();
|
this.stats.getBlockSummariesStats.requests.incrementAndGet();
|
||||||
|
|
||||||
// If peer's parent signature matches our latest block signature
|
// If peer's parent signature matches our latest block signature
|
||||||
// then we can short-circuit with an empty response
|
// then we have no blocks after that and can short-circuit with an empty response
|
||||||
BlockData chainTip = getChainTip();
|
BlockData chainTip = getChainTip();
|
||||||
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
|
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
|
||||||
Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
|
Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
|
||||||
|
? new BlockSummariesV2Message(Collections.emptyList())
|
||||||
|
: new BlockSummariesMessage(Collections.emptyList());
|
||||||
|
|
||||||
blockSummariesMessage.setId(message.getId());
|
blockSummariesMessage.setId(message.getId());
|
||||||
|
|
||||||
if (!peer.sendMessage(blockSummariesMessage))
|
if (!peer.sendMessage(blockSummariesMessage))
|
||||||
peer.disconnect("failed to send block summaries");
|
peer.disconnect("failed to send block summaries");
|
||||||
|
|
||||||
@ -1467,7 +1520,9 @@ public class Controller extends Thread {
|
|||||||
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
|
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
|
||||||
}
|
}
|
||||||
|
|
||||||
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
|
Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
|
||||||
|
? new BlockSummariesV2Message(blockSummaries)
|
||||||
|
: new BlockSummariesMessage(blockSummaries);
|
||||||
blockSummariesMessage.setId(message.getId());
|
blockSummariesMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(blockSummariesMessage))
|
if (!peer.sendMessage(blockSummariesMessage))
|
||||||
peer.disconnect("failed to send block summaries");
|
peer.disconnect("failed to send block summaries");
|
||||||
@ -1542,18 +1597,59 @@ public class Controller extends Thread {
|
|||||||
// If peer is inbound and we've not updated their height
|
// If peer is inbound and we've not updated their height
|
||||||
// then this is probably their initial HEIGHT_V2 message
|
// then this is probably their initial HEIGHT_V2 message
|
||||||
// so they need a corresponding HEIGHT_V2 message from us
|
// so they need a corresponding HEIGHT_V2 message from us
|
||||||
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
|
if (!peer.isOutbound() && peer.getChainTipData() == null) {
|
||||||
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
|
Message responseMessage = Network.getInstance().buildHeightOrChainTipInfo(peer);
|
||||||
|
|
||||||
|
if (responseMessage == null || !peer.sendMessage(responseMessage)) {
|
||||||
|
peer.disconnect("failed to send our chain tip info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update peer chain tip data
|
// Update peer chain tip data
|
||||||
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
|
BlockSummaryData newChainTipData = new BlockSummaryData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getMinterPublicKey(), heightV2Message.getTimestamp());
|
||||||
peer.setChainTipData(newChainTipData);
|
peer.setChainTipData(newChainTipData);
|
||||||
|
|
||||||
// Potentially synchronize
|
// Potentially synchronize
|
||||||
Synchronizer.getInstance().requestSync();
|
Synchronizer.getInstance().requestSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onNetworkBlockSummariesV2Message(Peer peer, Message message) {
|
||||||
|
BlockSummariesV2Message blockSummariesV2Message = (BlockSummariesV2Message) message;
|
||||||
|
|
||||||
|
if (!Settings.getInstance().isLite()) {
|
||||||
|
// If peer is inbound and we've not updated their height
|
||||||
|
// then this is probably their initial BLOCK_SUMMARIES_V2 message
|
||||||
|
// so they need a corresponding BLOCK_SUMMARIES_V2 message from us
|
||||||
|
if (!peer.isOutbound() && peer.getChainTipData() == null) {
|
||||||
|
Message responseMessage = Network.getInstance().buildHeightOrChainTipInfo(peer);
|
||||||
|
|
||||||
|
if (responseMessage == null || !peer.sendMessage(responseMessage)) {
|
||||||
|
peer.disconnect("failed to send our chain tip info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.hasId()) {
|
||||||
|
/*
|
||||||
|
* Experimental proof-of-concept: discard messages with ID
|
||||||
|
* These are 'late' reply messages received after timeout has expired,
|
||||||
|
* having been passed upwards from Peer to Network to Controller.
|
||||||
|
* Hence, these are NOT simple "here's my chain tip" broadcasts from other peers.
|
||||||
|
*/
|
||||||
|
LOGGER.debug("Discarding late {} message with ID {} from {}", message.getType().name(), message.getId(), peer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update peer chain tip data
|
||||||
|
peer.setChainTipSummaries(blockSummariesV2Message.getBlockSummaries());
|
||||||
|
|
||||||
|
// Potentially synchronize
|
||||||
|
Synchronizer.getInstance().requestSync();
|
||||||
|
}
|
||||||
|
|
||||||
private void onNetworkGetAccountMessage(Peer peer, Message message) {
|
private void onNetworkGetAccountMessage(Peer peer, Message message) {
|
||||||
GetAccountMessage getAccountMessage = (GetAccountMessage) message;
|
GetAccountMessage getAccountMessage = (GetAccountMessage) message;
|
||||||
String address = getAccountMessage.getAddress();
|
String address = getAccountMessage.getAddress();
|
||||||
@ -1569,8 +1665,8 @@ public class Controller extends Thread {
|
|||||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
// 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));
|
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
|
// Send generic 'unknown' message as it's very short
|
||||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
Message accountUnknownMessage = new GenericUnknownMessage();
|
||||||
accountUnknownMessage.setId(message.getId());
|
accountUnknownMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(accountUnknownMessage))
|
if (!peer.sendMessage(accountUnknownMessage))
|
||||||
peer.disconnect("failed to send account-unknown response");
|
peer.disconnect("failed to send account-unknown response");
|
||||||
@ -1605,8 +1701,8 @@ public class Controller extends Thread {
|
|||||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
// 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));
|
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
|
// Send generic 'unknown' message as it's very short
|
||||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
Message accountUnknownMessage = new GenericUnknownMessage();
|
||||||
accountUnknownMessage.setId(message.getId());
|
accountUnknownMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(accountUnknownMessage))
|
if (!peer.sendMessage(accountUnknownMessage))
|
||||||
peer.disconnect("failed to send account-unknown response");
|
peer.disconnect("failed to send account-unknown response");
|
||||||
@ -1649,8 +1745,8 @@ public class Controller extends Thread {
|
|||||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
// 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));
|
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
|
// Send generic 'unknown' message as it's very short
|
||||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
Message accountUnknownMessage = new GenericUnknownMessage();
|
||||||
accountUnknownMessage.setId(message.getId());
|
accountUnknownMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(accountUnknownMessage))
|
if (!peer.sendMessage(accountUnknownMessage))
|
||||||
peer.disconnect("failed to send account-unknown response");
|
peer.disconnect("failed to send account-unknown response");
|
||||||
@ -1686,8 +1782,8 @@ public class Controller extends Thread {
|
|||||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
// 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));
|
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
|
// Send generic 'unknown' message as it's very short
|
||||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
Message accountUnknownMessage = new GenericUnknownMessage();
|
||||||
accountUnknownMessage.setId(message.getId());
|
accountUnknownMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(accountUnknownMessage))
|
if (!peer.sendMessage(accountUnknownMessage))
|
||||||
peer.disconnect("failed to send account-unknown response");
|
peer.disconnect("failed to send account-unknown response");
|
||||||
@ -1721,8 +1817,8 @@ public class Controller extends Thread {
|
|||||||
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
// 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));
|
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
|
// Send generic 'unknown' message as it's very short
|
||||||
Message nameUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
Message nameUnknownMessage = new GenericUnknownMessage();
|
||||||
nameUnknownMessage.setId(message.getId());
|
nameUnknownMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(nameUnknownMessage))
|
if (!peer.sendMessage(nameUnknownMessage))
|
||||||
peer.disconnect("failed to send name-unknown response");
|
peer.disconnect("failed to send name-unknown response");
|
||||||
@ -1770,14 +1866,14 @@ public class Controller extends Thread {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
if (peerChainTipData == null) {
|
if (peerChainTipData == null) {
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disregard peers that don't have a recent block
|
// Disregard peers that don't have a recent block
|
||||||
if (peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp) {
|
if (peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp) {
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1805,6 +1901,10 @@ public class Controller extends Thread {
|
|||||||
if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp)
|
if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (Settings.getInstance().isSingleNodeTestnet())
|
||||||
|
// Single node testnets won't have peers, so we can assume up to date from this point
|
||||||
|
return true;
|
||||||
|
|
||||||
// Needs a mutable copy of the unmodifiableList
|
// Needs a mutable copy of the unmodifiableList
|
||||||
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||||
if (peers == null)
|
if (peers == null)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,404 @@
|
|||||||
|
package org.qortal.controller;
|
||||||
|
|
||||||
|
import com.rust.litewalletjni.LiteWalletJni;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||||
|
import org.qortal.arbitrary.exception.MissingDataException;
|
||||||
|
import org.qortal.crosschain.ForeignBlockchainException;
|
||||||
|
import org.qortal.crosschain.PirateWallet;
|
||||||
|
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||||
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.network.Network;
|
||||||
|
import org.qortal.network.Peer;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.transaction.ArbitraryTransaction;
|
||||||
|
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.FilesystemUtils;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class PirateChainWalletController extends Thread {
|
||||||
|
|
||||||
|
protected static final Logger LOGGER = LogManager.getLogger(PirateChainWalletController.class);
|
||||||
|
|
||||||
|
private static PirateChainWalletController instance;
|
||||||
|
|
||||||
|
final private static long SAVE_INTERVAL = 60 * 60 * 1000L; // 1 hour
|
||||||
|
private long lastSaveTime = 0L;
|
||||||
|
|
||||||
|
private boolean running;
|
||||||
|
private PirateWallet currentWallet = null;
|
||||||
|
private boolean shouldLoadWallet = false;
|
||||||
|
private String loadStatus = null;
|
||||||
|
|
||||||
|
private static String qdnWalletSignature = "EsfUw54perxkEtfoUoL7Z97XPrNsZRZXePVZPz3cwRm9qyEPSofD5KmgVpDqVitQp7LhnZRmL6z2V9hEe1YS45T";
|
||||||
|
|
||||||
|
|
||||||
|
private PirateChainWalletController() {
|
||||||
|
this.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PirateChainWalletController getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = new PirateChainWalletController();
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Thread.currentThread().setName("Pirate Chain Wallet Controller");
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (running && !Controller.isStopping()) {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
|
||||||
|
// Wait until we have a request to load the wallet
|
||||||
|
if (!shouldLoadWallet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LiteWalletJni.isLoaded()) {
|
||||||
|
this.loadLibrary();
|
||||||
|
|
||||||
|
// If still not loaded, sleep to prevent too many requests
|
||||||
|
if (!LiteWalletJni.isLoaded()) {
|
||||||
|
Thread.sleep(5 * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallet is downloaded, so clear the status
|
||||||
|
this.loadStatus = null;
|
||||||
|
|
||||||
|
if (this.currentWallet == null) {
|
||||||
|
// Nothing to do yet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this.currentWallet.isNullSeedWallet()) {
|
||||||
|
// Don't sync the null seed wallet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.debug("Syncing Pirate Chain wallet...");
|
||||||
|
String response = LiteWalletJni.execute("sync", "");
|
||||||
|
LOGGER.debug("sync response: {}", response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
if (json.has("result")) {
|
||||||
|
String result = json.getString("result");
|
||||||
|
|
||||||
|
// We may have to set wallet to ready if this is the first ever successful sync
|
||||||
|
if (Objects.equals(result, "success")) {
|
||||||
|
this.currentWallet.setReady(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
LOGGER.info("Unable to interpret JSON", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit sync attempts
|
||||||
|
Thread.sleep(30000);
|
||||||
|
|
||||||
|
// Save wallet if needed
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
if (now != null && now-SAVE_INTERVAL >= this.lastSaveTime) {
|
||||||
|
this.saveCurrentWallet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Fall-through to exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
// Save the wallet
|
||||||
|
this.saveCurrentWallet();
|
||||||
|
|
||||||
|
this.running = false;
|
||||||
|
this.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// QDN & wallet libraries
|
||||||
|
|
||||||
|
private void loadLibrary() throws InterruptedException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Check if architecture is supported
|
||||||
|
String libFileName = PirateChainWalletController.getRustLibFilename();
|
||||||
|
if (libFileName == null) {
|
||||||
|
String osName = System.getProperty("os.name");
|
||||||
|
String osArchitecture = System.getProperty("os.arch");
|
||||||
|
this.loadStatus = String.format("Unsupported architecture (%s %s)", osName, osArchitecture);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the library exists in the wallets folder
|
||||||
|
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
|
||||||
|
Path libPath = Paths.get(libDirectory.toString(), libFileName);
|
||||||
|
if (Files.exists(libPath)) {
|
||||||
|
// Already downloaded; we can load the library right away
|
||||||
|
LiteWalletJni.loadLibrary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Library not found, so check if we've fetched the resource from QDN
|
||||||
|
ArbitraryTransactionData t = this.getTransactionData(repository);
|
||||||
|
if (t == null) {
|
||||||
|
// Can't find the transaction - maybe on a different chain?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until we have a sufficient number of peers to attempt QDN downloads
|
||||||
|
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
|
||||||
|
if (handshakedPeers.size() < Settings.getInstance().getMinBlockchainPeers()) {
|
||||||
|
// Wait for more peers
|
||||||
|
this.loadStatus = String.format("Searching for peers...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build resource
|
||||||
|
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(t.getName(),
|
||||||
|
ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier());
|
||||||
|
try {
|
||||||
|
arbitraryDataReader.loadSynchronously(false);
|
||||||
|
} catch (MissingDataException e) {
|
||||||
|
LOGGER.info("Missing data when loading Pirate Chain library");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check its status
|
||||||
|
ArbitraryResourceStatus status = ArbitraryTransactionUtils.getStatus(
|
||||||
|
t.getService(), t.getName(), t.getIdentifier(), false);
|
||||||
|
|
||||||
|
if (status.getStatus() != ArbitraryResourceStatus.Status.READY) {
|
||||||
|
LOGGER.info("Not ready yet: {}", status.getTitle());
|
||||||
|
this.loadStatus = String.format("Downloading files from QDN... (%d / %d)", status.getLocalChunkCount(), status.getTotalChunkCount());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files are downloaded, so copy the necessary files to the wallets folder
|
||||||
|
// Delete the wallets/*/lib directory first, in case earlier versions of the wallet are present
|
||||||
|
Path walletsLibDirectory = PirateChainWalletController.getWalletsLibDirectory();
|
||||||
|
if (Files.exists(walletsLibDirectory)) {
|
||||||
|
FilesystemUtils.safeDeleteDirectory(walletsLibDirectory, false);
|
||||||
|
}
|
||||||
|
Files.createDirectories(libDirectory);
|
||||||
|
FileUtils.copyDirectory(arbitraryDataReader.getFilePath().toFile(), libDirectory.toFile());
|
||||||
|
|
||||||
|
// Clear reader cache so only one copy exists
|
||||||
|
ArbitraryDataResource resource = new ArbitraryDataResource(t.getName(),
|
||||||
|
ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier());
|
||||||
|
resource.deleteCache();
|
||||||
|
|
||||||
|
// Finally, load the library
|
||||||
|
LiteWalletJni.loadLibrary();
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error("Repository issue when loading Pirate Chain library", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Error when loading Pirate Chain library", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArbitraryTransactionData getTransactionData(Repository repository) {
|
||||||
|
try {
|
||||||
|
byte[] signature = Base58.decode(qdnWalletSignature);
|
||||||
|
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||||
|
if (!(transactionData instanceof ArbitraryTransactionData))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||||
|
if (arbitraryTransaction != null) {
|
||||||
|
return (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (DataException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRustLibFilename() {
|
||||||
|
String osName = System.getProperty("os.name");
|
||||||
|
String osArchitecture = System.getProperty("os.arch");
|
||||||
|
|
||||||
|
if (osName.equals("Mac OS X") && osArchitecture.equals("x86_64")) {
|
||||||
|
return "librust-macos-x86_64.dylib";
|
||||||
|
}
|
||||||
|
else if ((osName.equals("Linux") || osName.equals("FreeBSD")) && osArchitecture.equals("aarch64")) {
|
||||||
|
return "librust-linux-aarch64.so";
|
||||||
|
}
|
||||||
|
else if ((osName.equals("Linux") || osName.equals("FreeBSD")) && osArchitecture.equals("amd64")) {
|
||||||
|
return "librust-linux-x86_64.so";
|
||||||
|
}
|
||||||
|
else if (osName.contains("Windows") && osArchitecture.equals("amd64")) {
|
||||||
|
return "librust-windows-x86_64.dll";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path getWalletsLibDirectory() {
|
||||||
|
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path getRustLibOuterDirectory() {
|
||||||
|
String sigPrefix = qdnWalletSignature.substring(0, 8);
|
||||||
|
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib", sigPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Wallet functions
|
||||||
|
|
||||||
|
public boolean initWithEntropy58(String entropy58) {
|
||||||
|
return this.initWithEntropy58(entropy58, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean initNullSeedWallet() {
|
||||||
|
return this.initWithEntropy58(Base58.encode(new byte[32]), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean initWithEntropy58(String entropy58, boolean isNullSeedWallet) {
|
||||||
|
// If the JNI library isn't loaded yet then we can't proceed
|
||||||
|
if (!LiteWalletJni.isLoaded()) {
|
||||||
|
shouldLoadWallet = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] entropyBytes = Base58.decode(entropy58);
|
||||||
|
|
||||||
|
if (entropyBytes == null || entropyBytes.length != 32) {
|
||||||
|
LOGGER.info("Invalid entropy bytes");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentWallet != null) {
|
||||||
|
if (this.currentWallet.entropyBytesEqual(entropyBytes)) {
|
||||||
|
// Wallet already active - nothing to do
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Different wallet requested - close the existing one and switch over
|
||||||
|
this.closeCurrentWallet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.currentWallet = new PirateWallet(entropyBytes, isNullSeedWallet);
|
||||||
|
if (!this.currentWallet.isReady()) {
|
||||||
|
// Don't persist wallets that aren't ready
|
||||||
|
this.currentWallet = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.info("Unable to initialize wallet: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCurrentWallet() {
|
||||||
|
if (this.currentWallet == null) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (this.currentWallet.save()) {
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
if (now != null) {
|
||||||
|
this.lastSaveTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.info("Unable to save wallet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PirateWallet getCurrentWallet() {
|
||||||
|
return this.currentWallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeCurrentWallet() {
|
||||||
|
this.saveCurrentWallet();
|
||||||
|
this.currentWallet = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureInitialized() throws ForeignBlockchainException {
|
||||||
|
if (!LiteWalletJni.isLoaded() || this.currentWallet == null || !this.currentWallet.isInitialized()) {
|
||||||
|
throw new ForeignBlockchainException("Pirate wallet isn't initialized yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureNotNullSeed() throws ForeignBlockchainException {
|
||||||
|
// Safety check to make sure funds aren't sent to a null seed wallet
|
||||||
|
if (this.currentWallet == null || this.currentWallet.isNullSeedWallet()) {
|
||||||
|
throw new ForeignBlockchainException("Invalid wallet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureSynchronized() throws ForeignBlockchainException {
|
||||||
|
if (this.currentWallet == null || !this.currentWallet.isSynchronized()) {
|
||||||
|
throw new ForeignBlockchainException("Wallet isn't synchronized yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
String response = LiteWalletJni.execute("syncStatus", "");
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
if (json.has("syncing")) {
|
||||||
|
boolean isSyncing = Boolean.valueOf(json.getString("syncing"));
|
||||||
|
if (isSyncing) {
|
||||||
|
long syncedBlocks = json.getLong("synced_blocks");
|
||||||
|
long totalBlocks = json.getLong("total_blocks");
|
||||||
|
|
||||||
|
throw new ForeignBlockchainException(String.format("Sync in progress (%d / %d). Please try again later.", syncedBlocks, totalBlocks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSyncStatus() {
|
||||||
|
if (this.currentWallet == null || !this.currentWallet.isInitialized()) {
|
||||||
|
if (this.loadStatus != null) {
|
||||||
|
return this.loadStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Not initialized yet";
|
||||||
|
}
|
||||||
|
|
||||||
|
String syncStatusResponse = LiteWalletJni.execute("syncStatus", "");
|
||||||
|
org.json.JSONObject json = new JSONObject(syncStatusResponse);
|
||||||
|
if (json.has("syncing")) {
|
||||||
|
boolean isSyncing = Boolean.valueOf(json.getString("syncing"));
|
||||||
|
if (isSyncing) {
|
||||||
|
long syncedBlocks = json.getLong("synced_blocks");
|
||||||
|
long totalBlocks = json.getLong("total_blocks");
|
||||||
|
return String.format("Sync in progress (%d / %d)", syncedBlocks, totalBlocks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSynchronized = this.currentWallet.isSynchronized();
|
||||||
|
if (isSynchronized) {
|
||||||
|
return "Synchronized";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Initializing wallet...";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -19,7 +19,6 @@ import org.qortal.block.BlockChain;
|
|||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
import org.qortal.data.block.BlockSummaryData;
|
import org.qortal.data.block.BlockSummaryData;
|
||||||
import org.qortal.data.block.CommonBlockData;
|
import org.qortal.data.block.CommonBlockData;
|
||||||
import org.qortal.data.network.PeerChainTipData;
|
|
||||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.event.Event;
|
import org.qortal.event.Event;
|
||||||
@ -54,7 +53,8 @@ public class Synchronizer extends Thread {
|
|||||||
/** Maximum number of block signatures we ask from peer in one go */
|
/** Maximum number of block signatures we ask from peer in one go */
|
||||||
private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings?
|
private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings?
|
||||||
|
|
||||||
private static final long RECOVERY_MODE_TIMEOUT = 10 * 60 * 1000L; // ms
|
/** Maximum number of consecutive failed sync attempts before marking peer as misbehaved */
|
||||||
|
private static final int MAX_CONSECUTIVE_FAILED_SYNC_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
|
||||||
private boolean running;
|
private boolean running;
|
||||||
@ -76,12 +76,14 @@ public class Synchronizer extends Thread {
|
|||||||
private volatile boolean isSynchronizing = false;
|
private volatile boolean isSynchronizing = false;
|
||||||
/** Temporary estimate of synchronization progress for SysTray use. */
|
/** Temporary estimate of synchronization progress for SysTray use. */
|
||||||
private volatile int syncPercent = 0;
|
private volatile int syncPercent = 0;
|
||||||
|
/** Temporary estimate of blocks remaining for SysTray use. */
|
||||||
|
private volatile int blocksRemaining = 0;
|
||||||
|
|
||||||
private static volatile boolean requestSync = false;
|
private static volatile boolean requestSync = false;
|
||||||
private boolean syncRequestPending = false;
|
private boolean syncRequestPending = false;
|
||||||
|
|
||||||
// Keep track of invalid blocks so that we don't keep trying to sync them
|
// 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 timeValidBlockLastReceived = null;
|
||||||
public Long timeInvalidBlockLastReceived = null;
|
public Long timeInvalidBlockLastReceived = null;
|
||||||
|
|
||||||
@ -181,6 +183,18 @@ public class Synchronizer extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getBlocksRemaining() {
|
||||||
|
synchronized (this.syncLock) {
|
||||||
|
// Report as 0 blocks remaining if the latest block is within the last 60 mins
|
||||||
|
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||||
|
if (Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.isSynchronizing ? this.blocksRemaining : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void requestSync() {
|
public void requestSync() {
|
||||||
requestSync = true;
|
requestSync = true;
|
||||||
}
|
}
|
||||||
@ -233,6 +247,9 @@ public class Synchronizer extends Thread {
|
|||||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||||
peers.removeIf(Controller.hasInferiorChainTip);
|
peers.removeIf(Controller.hasInferiorChainTip);
|
||||||
|
|
||||||
|
// Disregard peers that have a block with an invalid signer
|
||||||
|
peers.removeIf(Controller.hasInvalidSigner);
|
||||||
|
|
||||||
final int peersBeforeComparison = peers.size();
|
final int peersBeforeComparison = peers.size();
|
||||||
|
|
||||||
// Request recent block summaries from the remaining peers, and locate our common block with each
|
// Request recent block summaries from the remaining peers, and locate our common block with each
|
||||||
@ -282,7 +299,7 @@ public class Synchronizer extends Thread {
|
|||||||
BlockData priorChainTip = Controller.getInstance().getChainTip();
|
BlockData priorChainTip = Controller.getInstance().getChainTip();
|
||||||
|
|
||||||
synchronized (this.syncLock) {
|
synchronized (this.syncLock) {
|
||||||
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getHeight();
|
||||||
|
|
||||||
// Only update SysTray if we're potentially changing height
|
// Only update SysTray if we're potentially changing height
|
||||||
if (this.syncPercent < 100) {
|
if (this.syncPercent < 100) {
|
||||||
@ -312,7 +329,7 @@ public class Synchronizer extends Thread {
|
|||||||
|
|
||||||
case INFERIOR_CHAIN: {
|
case INFERIOR_CHAIN: {
|
||||||
// Update our list of inferior chain tips
|
// Update our list of inferior chain tips
|
||||||
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
|
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getSignature());
|
||||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||||
inferiorChainSignatures.add(inferiorChainSignature);
|
inferiorChainSignatures.add(inferiorChainSignature);
|
||||||
|
|
||||||
@ -320,7 +337,8 @@ public class Synchronizer extends Thread {
|
|||||||
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
|
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||||
|
|
||||||
// Notify peer of our superior chain
|
// Notify peer of our superior chain
|
||||||
if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, priorChainTip)))
|
Message message = Network.getInstance().buildHeightOrChainTipInfo(peer);
|
||||||
|
if (message == null || !peer.sendMessage(message))
|
||||||
peer.disconnect("failed to notify peer of our superior chain");
|
peer.disconnect("failed to notify peer of our superior chain");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -341,7 +359,7 @@ public class Synchronizer extends Thread {
|
|||||||
// fall-through...
|
// fall-through...
|
||||||
case NOTHING_TO_DO: {
|
case NOTHING_TO_DO: {
|
||||||
// Update our list of inferior chain tips
|
// Update our list of inferior chain tips
|
||||||
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
|
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getSignature());
|
||||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||||
inferiorChainSignatures.add(inferiorChainSignature);
|
inferiorChainSignatures.add(inferiorChainSignature);
|
||||||
|
|
||||||
@ -369,8 +387,7 @@ public class Synchronizer extends Thread {
|
|||||||
// Reset our cache of inferior chains
|
// Reset our cache of inferior chains
|
||||||
inferiorChainSignatures.clear();
|
inferiorChainSignatures.clear();
|
||||||
|
|
||||||
Network network = Network.getInstance();
|
Network.getInstance().broadcastOurChain();
|
||||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
|
||||||
|
|
||||||
EventBus.INSTANCE.notify(new NewChainTipEvent(priorChainTip, newChainTip));
|
EventBus.INSTANCE.notify(new NewChainTipEvent(priorChainTip, newChainTip));
|
||||||
}
|
}
|
||||||
@ -397,9 +414,10 @@ public class Synchronizer extends Thread {
|
|||||||
timePeersLastAvailable = NTP.getTime();
|
timePeersLastAvailable = NTP.getTime();
|
||||||
|
|
||||||
// If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint
|
// If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint
|
||||||
if (NTP.getTime() - timePeersLastAvailable > RECOVERY_MODE_TIMEOUT) {
|
long recoveryModeTimeout = Settings.getInstance().getRecoveryModeTimeout();
|
||||||
|
if (NTP.getTime() - timePeersLastAvailable > recoveryModeTimeout) {
|
||||||
if (recoveryMode == false) {
|
if (recoveryMode == false) {
|
||||||
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", RECOVERY_MODE_TIMEOUT/60/1000));
|
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", recoveryModeTimeout/60/1000));
|
||||||
recoveryMode = true;
|
recoveryMode = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -513,13 +531,13 @@ public class Synchronizer extends Thread {
|
|||||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
final int ourInitialHeight = ourLatestBlockData.getHeight();
|
final int ourInitialHeight = ourLatestBlockData.getHeight();
|
||||||
|
|
||||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
int peerHeight = peerChainTipData.getLastHeight();
|
int peerHeight = peerChainTipData.getHeight();
|
||||||
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
byte[] peersLastBlockSignature = peerChainTipData.getSignature();
|
||||||
|
|
||||||
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
|
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
|
||||||
LOGGER.debug(String.format("Fetching summaries from peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
|
LOGGER.debug(String.format("Fetching summaries from peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
|
||||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
|
||||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
||||||
|
|
||||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||||
@ -617,7 +635,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.
|
// We have already determined that the correct chain diverged from a lower height. We are safe to skip these peers.
|
||||||
for (Peer peer : peersSharingCommonBlock) {
|
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));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
@ -628,16 +646,18 @@ public class Synchronizer extends Thread {
|
|||||||
int minChainLength = this.calculateMinChainLengthOfPeers(peersSharingCommonBlock, commonBlockSummary);
|
int minChainLength = this.calculateMinChainLengthOfPeers(peersSharingCommonBlock, commonBlockSummary);
|
||||||
|
|
||||||
// Fetch block summaries from each peer
|
// 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 we're shutting down, just return the latest peer list
|
||||||
if (Controller.isStopping())
|
if (Controller.isStopping())
|
||||||
return peers;
|
return peers;
|
||||||
|
|
||||||
// Count the number of blocks this peer has beyond our common block
|
// Count the number of blocks this peer has beyond our common block
|
||||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
final int peerHeight = peerChainTipData.getLastHeight();
|
final int peerHeight = peerChainTipData.getHeight();
|
||||||
final byte[] peerLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
final byte[] peerLastBlockSignature = peerChainTipData.getSignature();
|
||||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||||
// Limit the number of blocks we are comparing. FUTURE: we could request more in batches, but there may not be a case when this is needed
|
// Limit the number of blocks we are comparing. FUTURE: we could request more in batches, but there may not be a case when this is needed
|
||||||
int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
|
int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
|
||||||
@ -685,6 +705,8 @@ public class Synchronizer extends Thread {
|
|||||||
if (this.containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
|
if (this.containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
|
||||||
LOGGER.debug("Ignoring peer %s because it holds an invalid block", peer);
|
LOGGER.debug("Ignoring peer %s because it holds an invalid block", peer);
|
||||||
peers.remove(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
|
// 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
|
||||||
@ -723,8 +745,9 @@ public class Synchronizer extends Thread {
|
|||||||
|
|
||||||
LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
|
LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
|
||||||
for (Peer peer : peersSharingCommonBlock) {
|
for (Peer peer : peersSharingCommonBlock) {
|
||||||
final int peerHeight = peer.getChainTipData().getLastHeight();
|
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
|
final int peerHeight = peerChainTipData.getHeight();
|
||||||
|
final Long peerLastBlockTimestamp = peerChainTipData.getTimestamp();
|
||||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||||
final CommonBlockData peerCommonBlockData = peer.getCommonBlockData();
|
final CommonBlockData peerCommonBlockData = peer.getCommonBlockData();
|
||||||
|
|
||||||
@ -821,7 +844,7 @@ public class Synchronizer extends Thread {
|
|||||||
// Calculate the length of the shortest peer chain sharing this common block
|
// Calculate the length of the shortest peer chain sharing this common block
|
||||||
int minChainLength = 0;
|
int minChainLength = 0;
|
||||||
for (Peer peer : peersSharingCommonBlock) {
|
for (Peer peer : peersSharingCommonBlock) {
|
||||||
final int peerHeight = peer.getChainTipData().getLastHeight();
|
final int peerHeight = peer.getChainTipData().getHeight();
|
||||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||||
|
|
||||||
if (peerAdditionalBlocksAfterCommonBlock < minChainLength || minChainLength == 0)
|
if (peerAdditionalBlocksAfterCommonBlock < minChainLength || minChainLength == 0)
|
||||||
@ -840,6 +863,10 @@ public class Synchronizer extends Thread {
|
|||||||
|
|
||||||
/* Invalid block signature tracking */
|
/* Invalid block signature tracking */
|
||||||
|
|
||||||
|
public Map<ByteArray, Long> getInvalidBlockSignatures() {
|
||||||
|
return this.invalidBlockSignatures;
|
||||||
|
}
|
||||||
|
|
||||||
private void addInvalidBlockSignature(byte[] signature) {
|
private void addInvalidBlockSignature(byte[] signature) {
|
||||||
Long now = NTP.getTime();
|
Long now = NTP.getTime();
|
||||||
if (now == null) {
|
if (now == null) {
|
||||||
@ -847,8 +874,7 @@ public class Synchronizer extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add or update existing entry
|
// Add or update existing entry
|
||||||
String sig58 = Base58.encode(signature);
|
invalidBlockSignatures.put(ByteArray.wrap(signature), now);
|
||||||
invalidBlockSignatures.put(sig58, now);
|
|
||||||
}
|
}
|
||||||
private void deleteOlderInvalidSignatures(Long now) {
|
private void deleteOlderInvalidSignatures(Long now) {
|
||||||
if (now == null) {
|
if (now == null) {
|
||||||
@ -867,17 +893,16 @@ public class Synchronizer extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private boolean containsInvalidBlockSummary(List<BlockSummaryData> blockSummaries) {
|
public boolean containsInvalidBlockSummary(List<BlockSummaryData> blockSummaries) {
|
||||||
if (blockSummaries == null || invalidBlockSignatures == null) {
|
if (blockSummaries == null || invalidBlockSignatures == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through our known invalid blocks and check each one against supplied block summaries
|
// Loop through our known invalid blocks and check each one against supplied block summaries
|
||||||
for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
|
for (ByteArray invalidSignature : invalidBlockSignatures.keySet()) {
|
||||||
byte[] invalidSignature = Base58.decode(invalidSignature58);
|
|
||||||
for (BlockSummaryData blockSummary : blockSummaries) {
|
for (BlockSummaryData blockSummary : blockSummaries) {
|
||||||
byte[] signature = blockSummary.getSignature();
|
byte[] signature = blockSummary.getSignature();
|
||||||
if (Arrays.equals(signature, invalidSignature)) {
|
if (Arrays.equals(signature, invalidSignature.value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -890,10 +915,9 @@ public class Synchronizer extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loop through our known invalid blocks and check each one against supplied block signatures
|
// Loop through our known invalid blocks and check each one against supplied block signatures
|
||||||
for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
|
for (ByteArray invalidSignature : invalidBlockSignatures.keySet()) {
|
||||||
byte[] invalidSignature = Base58.decode(invalidSignature58);
|
|
||||||
for (byte[] signature : blockSignatures) {
|
for (byte[] signature : blockSignatures) {
|
||||||
if (Arrays.equals(signature, invalidSignature)) {
|
if (Arrays.equals(signature, invalidSignature.value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -928,13 +952,13 @@ public class Synchronizer extends Thread {
|
|||||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
final int ourInitialHeight = ourLatestBlockData.getHeight();
|
final int ourInitialHeight = ourLatestBlockData.getHeight();
|
||||||
|
|
||||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
int peerHeight = peerChainTipData.getLastHeight();
|
int peerHeight = peerChainTipData.getHeight();
|
||||||
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
byte[] peersLastBlockSignature = peerChainTipData.getSignature();
|
||||||
|
|
||||||
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
|
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
|
||||||
String syncString = String.format("Synchronizing with peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
|
String syncString = String.format("Synchronizing with peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
|
||||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
|
||||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp());
|
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp());
|
||||||
LOGGER.info(syncString);
|
LOGGER.info(syncString);
|
||||||
|
|
||||||
@ -1097,6 +1121,7 @@ public class Synchronizer extends Thread {
|
|||||||
// If common block is too far behind us then we're on massively different forks so give up.
|
// If common block is too far behind us then we're on massively different forks so give up.
|
||||||
if (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
|
if (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
|
||||||
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
||||||
|
peer.setLastTooDivergentTime(NTP.getTime());
|
||||||
return SynchronizationResult.TOO_DIVERGENT;
|
return SynchronizationResult.TOO_DIVERGENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1106,6 +1131,9 @@ public class Synchronizer extends Thread {
|
|||||||
testHeight = Math.max(testHeight - step, 1);
|
testHeight = Math.max(testHeight - step, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Peer not considered too divergent
|
||||||
|
peer.setLastTooDivergentTime(0L);
|
||||||
|
|
||||||
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
|
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
|
||||||
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
|
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
|
||||||
blockSummariesFromCommon.add(0, testBlockSummary);
|
blockSummariesFromCommon.add(0, testBlockSummary);
|
||||||
@ -1241,7 +1269,14 @@ public class Synchronizer extends Thread {
|
|||||||
int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size();
|
int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size();
|
||||||
|
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
while (height < peerHeight) {
|
|
||||||
|
// Keep fetching blocks from peer until we reach their tip, or reach a count of MAXIMUM_COMMON_DELTA blocks.
|
||||||
|
// We need to limit the total number, otherwise too much can be loaded into memory, causing an
|
||||||
|
// OutOfMemoryException. This is common when syncing from 1000+ blocks behind the chain tip, after starting
|
||||||
|
// from a small fork that didn't become part of the main chain. This causes the entire sync process to
|
||||||
|
// use syncToPeerChain(), resulting in potentially thousands of blocks being held in memory if the limit
|
||||||
|
// below isn't applied.
|
||||||
|
while (height < peerHeight && peerBlocks.size() <= MAXIMUM_COMMON_DELTA) {
|
||||||
if (Controller.isStopping())
|
if (Controller.isStopping())
|
||||||
return SynchronizationResult.SHUTTING_DOWN;
|
return SynchronizationResult.SHUTTING_DOWN;
|
||||||
|
|
||||||
@ -1308,7 +1343,7 @@ public class Synchronizer extends Thread {
|
|||||||
// Final check to make sure the peer isn't out of date (except for when we're in recovery mode)
|
// Final check to make sure the peer isn't out of date (except for when we're in recovery mode)
|
||||||
if (!recoveryMode && peer.getChainTipData() != null) {
|
if (!recoveryMode && peer.getChainTipData() != null) {
|
||||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||||
final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
|
final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
|
||||||
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {
|
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {
|
||||||
LOGGER.info(String.format("Peer %s is out of date, so abandoning sync attempt", peer));
|
LOGGER.info(String.format("Peer %s is out of date, so abandoning sync attempt", peer));
|
||||||
return SynchronizationResult.CHAIN_TIP_TOO_OLD;
|
return SynchronizationResult.CHAIN_TIP_TOO_OLD;
|
||||||
@ -1443,6 +1478,12 @@ public class Synchronizer extends Thread {
|
|||||||
|
|
||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
|
|
||||||
|
synchronized (this.syncLock) {
|
||||||
|
if (peer.getChainTipData() != null) {
|
||||||
|
this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1538,6 +1579,12 @@ public class Synchronizer extends Thread {
|
|||||||
|
|
||||||
repository.saveChanges();
|
repository.saveChanges();
|
||||||
|
|
||||||
|
synchronized (this.syncLock) {
|
||||||
|
if (peer.getChainTipData() != null) {
|
||||||
|
this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1548,12 +1595,19 @@ public class Synchronizer extends Thread {
|
|||||||
Message getBlockSummariesMessage = new GetBlockSummariesMessage(parentSignature, numberRequested);
|
Message getBlockSummariesMessage = new GetBlockSummariesMessage(parentSignature, numberRequested);
|
||||||
|
|
||||||
Message message = peer.getResponse(getBlockSummariesMessage);
|
Message message = peer.getResponse(getBlockSummariesMessage);
|
||||||
if (message == null || message.getType() != MessageType.BLOCK_SUMMARIES)
|
if (message == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
BlockSummariesMessage blockSummariesMessage = (BlockSummariesMessage) message;
|
if (message.getType() == MessageType.BLOCK_SUMMARIES) {
|
||||||
|
BlockSummariesMessage blockSummariesMessage = (BlockSummariesMessage) message;
|
||||||
|
return blockSummariesMessage.getBlockSummaries();
|
||||||
|
}
|
||||||
|
else if (message.getType() == MessageType.BLOCK_SUMMARIES_V2) {
|
||||||
|
BlockSummariesV2Message blockSummariesMessage = (BlockSummariesV2Message) message;
|
||||||
|
return blockSummariesMessage.getBlockSummaries();
|
||||||
|
}
|
||||||
|
|
||||||
return blockSummariesMessage.getBlockSummaries();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<byte[]> getBlockSignatures(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
|
private List<byte[]> getBlockSignatures(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
|
||||||
@ -1572,8 +1626,20 @@ public class Synchronizer extends Thread {
|
|||||||
Message getBlockMessage = new GetBlockMessage(signature);
|
Message getBlockMessage = new GetBlockMessage(signature);
|
||||||
|
|
||||||
Message message = peer.getResponse(getBlockMessage);
|
Message message = peer.getResponse(getBlockMessage);
|
||||||
if (message == null)
|
if (message == null) {
|
||||||
|
peer.getPeerData().incrementFailedSyncCount();
|
||||||
|
if (peer.getPeerData().getFailedSyncCount() >= MAX_CONSECUTIVE_FAILED_SYNC_ATTEMPTS) {
|
||||||
|
// Several failed attempts, so mark peer as misbehaved
|
||||||
|
LOGGER.info("Marking peer {} as misbehaved due to {} failed sync attempts", peer, peer.getPeerData().getFailedSyncCount());
|
||||||
|
Network.getInstance().peerMisbehaved(peer);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset failed sync count now that we have a block response
|
||||||
|
// FUTURE: we could move this to the end of the sync process, but to reduce risk this can be done
|
||||||
|
// at a later stage. For now we are only defending against serialization errors or no responses.
|
||||||
|
peer.getPeerData().setFailedSyncCount(0);
|
||||||
|
|
||||||
switch (message.getType()) {
|
switch (message.getType()) {
|
||||||
case BLOCK: {
|
case BLOCK: {
|
||||||
|
@ -67,6 +67,9 @@ public class ArbitraryDataFileListManager {
|
|||||||
/** Maximum number of hops that a file list relay request is allowed to make */
|
/** Maximum number of hops that a file list relay request is allowed to make */
|
||||||
public static int RELAY_REQUEST_MAX_HOPS = 4;
|
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() {
|
private ArbitraryDataFileListManager() {
|
||||||
}
|
}
|
||||||
@ -120,12 +123,22 @@ public class ArbitraryDataFileListManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then allow another 5 attempts, each 5 minutes apart
|
// Then allow another 3 attempts, each 5 minutes apart
|
||||||
if (timeSinceLastAttempt > 5 * 60 * 1000L) {
|
if (timeSinceLastAttempt > 5 * 60 * 1000L) {
|
||||||
// We haven't tried for at least 5 minutes
|
// We haven't tried for at least 5 minutes
|
||||||
|
|
||||||
if (networkBroadcastCount < 5) {
|
if (networkBroadcastCount < 6) {
|
||||||
// We've made less than 5 total attempts
|
// We've made less than 6 total attempts
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then allow another 4 attempts, each 30 minutes apart
|
||||||
|
if (timeSinceLastAttempt > 30 * 60 * 1000L) {
|
||||||
|
// We haven't tried for at least 5 minutes
|
||||||
|
|
||||||
|
if (networkBroadcastCount < 10) {
|
||||||
|
// We've made less than 10 total attempts
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,8 +197,8 @@ public class ArbitraryDataFileListManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeSinceLastAttempt > 24 * 60 * 60 * 1000L) {
|
if (timeSinceLastAttempt > 60 * 60 * 1000L) {
|
||||||
// We haven't tried for at least 24 hours
|
// We haven't tried for at least 1 hour
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,6 +537,7 @@ public class ArbitraryDataFileListManager {
|
|||||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
|
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
|
||||||
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
||||||
}
|
}
|
||||||
|
forwardArbitraryDataFileListMessage.setId(message.getId());
|
||||||
|
|
||||||
// Forward to requesting peer
|
// Forward to requesting peer
|
||||||
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
|
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
|
||||||
@ -694,9 +708,10 @@ public class ArbitraryDataFileListManager {
|
|||||||
|
|
||||||
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
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(
|
Network.getInstance().broadcast(
|
||||||
broadcastPeer -> broadcastPeer == peer ||
|
broadcastPeer ->
|
||||||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
|
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||||
? null : relayGetArbitraryDataFileListMessage);
|
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -82,7 +82,7 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use a fixed thread pool to execute the arbitrary data file requests
|
// Use a fixed thread pool to execute the arbitrary data file requests
|
||||||
int threadCount = 10;
|
int threadCount = 5;
|
||||||
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
||||||
for (int i = 0; i < threadCount; i++) {
|
for (int i = 0; i < threadCount; i++) {
|
||||||
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
||||||
@ -288,7 +288,7 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
// The ID needs to match that of the original request
|
// The ID needs to match that of the original request
|
||||||
message.setId(originalMessage.getId());
|
message.setId(originalMessage.getId());
|
||||||
|
|
||||||
if (!requestingPeer.sendMessage(message)) {
|
if (!requestingPeer.sendMessageWithTimeout(message, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||||
LOGGER.debug("Failed to forward arbitrary data file to peer {}", requestingPeer);
|
LOGGER.debug("Failed to forward arbitrary data file to peer {}", requestingPeer);
|
||||||
requestingPeer.disconnect("failed to forward arbitrary data file");
|
requestingPeer.disconnect("failed to forward arbitrary data file");
|
||||||
}
|
}
|
||||||
@ -564,13 +564,16 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
LOGGER.trace("Hash {} exists", hash58);
|
LOGGER.trace("Hash {} exists", hash58);
|
||||||
|
|
||||||
// We can serve the file directly as we already have it
|
// We can serve the file directly as we already have it
|
||||||
|
LOGGER.debug("Sending file {}...", arbitraryDataFile);
|
||||||
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
|
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
|
||||||
arbitraryDataFileMessage.setId(message.getId());
|
arbitraryDataFileMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(arbitraryDataFileMessage)) {
|
if (!peer.sendMessageWithTimeout(arbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||||
LOGGER.debug("Couldn't sent file");
|
LOGGER.debug("Couldn't send file {}", arbitraryDataFile);
|
||||||
peer.disconnect("failed to send file");
|
peer.disconnect("failed to send file");
|
||||||
}
|
}
|
||||||
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
else {
|
||||||
|
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (relayInfo != null) {
|
else if (relayInfo != null) {
|
||||||
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
||||||
@ -595,9 +598,10 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
|
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
|
||||||
LOGGER.debug(String.format("Sending 'file unknown' response to peer %s for GET_FILE request for unknown file %s", peer, arbitraryDataFile));
|
LOGGER.debug(String.format("Sending 'file unknown' response to peer %s for GET_FILE request for unknown file %s", peer, arbitraryDataFile));
|
||||||
|
|
||||||
// We'll send empty block summaries message as it's very short
|
// Send generic 'unknown' message as it's very short
|
||||||
// TODO: use a different message type here
|
Message fileUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
|
||||||
Message fileUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
? new GenericUnknownMessage()
|
||||||
|
: new BlockSummariesMessage(Collections.emptyList());
|
||||||
fileUnknownMessage.setId(message.getId());
|
fileUnknownMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(fileUnknownMessage)) {
|
if (!peer.sendMessage(fileUnknownMessage)) {
|
||||||
LOGGER.debug("Couldn't sent file-unknown response");
|
LOGGER.debug("Couldn't sent file-unknown response");
|
||||||
|
@ -48,7 +48,6 @@ public class ArbitraryDataStorageManager extends Thread {
|
|||||||
private List<ArbitraryTransactionData> hostedTransactions;
|
private List<ArbitraryTransactionData> hostedTransactions;
|
||||||
|
|
||||||
private String searchQuery;
|
private String searchQuery;
|
||||||
private List<ArbitraryTransactionData> searchResultsTransactions;
|
|
||||||
|
|
||||||
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
|
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
|
||||||
|
|
||||||
@ -344,11 +343,6 @@ public class ArbitraryDataStorageManager extends Thread {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public List<ArbitraryTransactionData> searchHostedTransactions(Repository repository, String query, Integer limit, Integer offset) {
|
public List<ArbitraryTransactionData> searchHostedTransactions(Repository repository, String query, Integer limit, Integer offset) {
|
||||||
// Load from results cache if we can (results that exists for the same query), to avoid disk reads
|
|
||||||
if (this.searchResultsTransactions != null && this.searchQuery.equals(query.toLowerCase())) {
|
|
||||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using cache if we can, to avoid disk reads
|
// Using cache if we can, to avoid disk reads
|
||||||
if (this.hostedTransactions == null) {
|
if (this.hostedTransactions == null) {
|
||||||
this.hostedTransactions = this.loadAllHostedTransactions(repository);
|
this.hostedTransactions = this.loadAllHostedTransactions(repository);
|
||||||
@ -376,10 +370,7 @@ public class ArbitraryDataStorageManager extends Thread {
|
|||||||
// Sort by newest first
|
// Sort by newest first
|
||||||
searchResultsList.sort(Comparator.comparingLong(ArbitraryTransactionData::getTimestamp).reversed());
|
searchResultsList.sort(Comparator.comparingLong(ArbitraryTransactionData::getTimestamp).reversed());
|
||||||
|
|
||||||
// Update cache
|
return ArbitraryTransactionUtils.limitOffsetTransactions(searchResultsList, limit, offset);
|
||||||
this.searchResultsTransactions = searchResultsList;
|
|
||||||
|
|
||||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,8 +22,7 @@ import org.qortal.utils.Triple;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.RELAY_REQUEST_MAX_DURATION;
|
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.*;
|
||||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.RELAY_REQUEST_MAX_HOPS;
|
|
||||||
|
|
||||||
public class ArbitraryMetadataManager {
|
public class ArbitraryMetadataManager {
|
||||||
|
|
||||||
@ -435,12 +434,13 @@ public class ArbitraryMetadataManager {
|
|||||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||||
|
|
||||||
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
|
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);
|
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||||
Network.getInstance().broadcast(
|
Network.getInstance().broadcast(
|
||||||
broadcastPeer -> broadcastPeer == peer ||
|
broadcastPeer ->
|
||||||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
|
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
|
||||||
? null : relayGetArbitraryMetadataMessage);
|
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -42,6 +42,7 @@ public class AtStatesPruner implements Runnable {
|
|||||||
|
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
repository.getATRepository().rebuildLatestAtStates();
|
repository.getATRepository().rebuildLatestAtStates();
|
||||||
|
repository.saveChanges();
|
||||||
|
|
||||||
while (!Controller.isStopping()) {
|
while (!Controller.isStopping()) {
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
|
@ -29,6 +29,7 @@ public class AtStatesTrimmer implements Runnable {
|
|||||||
|
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
repository.getATRepository().rebuildLatestAtStates();
|
repository.getATRepository().rebuildLatestAtStates();
|
||||||
|
repository.saveChanges();
|
||||||
|
|
||||||
while (!Controller.isStopping()) {
|
while (!Controller.isStopping()) {
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
|
@ -16,7 +16,7 @@ public class BlockArchiver implements Runnable {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiver.class);
|
private static final Logger LOGGER = LogManager.getLogger(BlockArchiver.class);
|
||||||
|
|
||||||
private static final long INITIAL_SLEEP_PERIOD = 0L; // TODO: 5 * 60 * 1000L + 1234L; // ms
|
private static final long INITIAL_SLEEP_PERIOD = 5 * 60 * 1000L + 1234L; // ms
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Block archiver");
|
Thread.currentThread().setName("Block archiver");
|
||||||
|
@ -102,6 +102,21 @@ public class NamesDatabaseIntegrityCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process CANCEL_SELL_NAME transactions
|
||||||
|
if (currentTransaction.getType() == TransactionType.CANCEL_SELL_NAME) {
|
||||||
|
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) currentTransaction;
|
||||||
|
Name nameObj = new Name(repository, cancelSellNameTransactionData.getName());
|
||||||
|
if (nameObj != null && nameObj.getNameData() != null) {
|
||||||
|
nameObj.cancelSell(cancelSellNameTransactionData);
|
||||||
|
modificationCount++;
|
||||||
|
LOGGER.trace("Processed CANCEL_SELL_NAME transaction for name {}", name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Something went wrong
|
||||||
|
throw new DataException(String.format("Name data not found for name %s", cancelSellNameTransactionData.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process BUY_NAME transactions
|
// Process BUY_NAME transactions
|
||||||
if (currentTransaction.getType() == TransactionType.BUY_NAME) {
|
if (currentTransaction.getType() == TransactionType.BUY_NAME) {
|
||||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
|
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
|
||||||
@ -128,7 +143,7 @@ public class NamesDatabaseIntegrityCheck {
|
|||||||
public int rebuildAllNames() {
|
public int rebuildAllNames() {
|
||||||
int modificationCount = 0;
|
int modificationCount = 0;
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
List<String> names = this.fetchAllNames(repository);
|
List<String> names = this.fetchAllNames(repository); // TODO: de-duplicate, to speed up this process
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
modificationCount += this.rebuildName(name, repository);
|
modificationCount += this.rebuildName(name, repository);
|
||||||
}
|
}
|
||||||
@ -326,6 +341,10 @@ public class NamesDatabaseIntegrityCheck {
|
|||||||
TransactionType.BUY_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
TransactionType.BUY_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
||||||
signatures.addAll(buyNameTransactions);
|
signatures.addAll(buyNameTransactions);
|
||||||
|
|
||||||
|
List<byte[]> cancelSellNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||||
|
TransactionType.CANCEL_SELL_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
||||||
|
signatures.addAll(cancelSellNameTransactions);
|
||||||
|
|
||||||
List<TransactionData> transactions = new ArrayList<>();
|
List<TransactionData> transactions = new ArrayList<>();
|
||||||
for (byte[] signature : signatures) {
|
for (byte[] signature : signatures) {
|
||||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||||
@ -390,6 +409,12 @@ public class NamesDatabaseIntegrityCheck {
|
|||||||
names.add(sellNameTransactionData.getName());
|
names.add(sellNameTransactionData.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ((transactionData instanceof CancelSellNameTransactionData)) {
|
||||||
|
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData;
|
||||||
|
if (!names.contains(cancelSellNameTransactionData.getName())) {
|
||||||
|
names.add(cancelSellNameTransactionData.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
@ -1,885 +0,0 @@
|
|||||||
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 LitecoinACCTv2TradeBot implements AcctTradeBot {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(LitecoinACCTv2TradeBot.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 LitecoinACCTv2TradeBot 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 LitecoinACCTv2TradeBot() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static synchronized LitecoinACCTv2TradeBot getInstance() {
|
|
||||||
if (instance == null)
|
|
||||||
instance = new LitecoinACCTv2TradeBot();
|
|
||||||
|
|
||||||
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 LTC.
|
|
||||||
* <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 Litecoin) 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'/Litecoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
|
||||||
* <li>QORT amount on offer by Bob</li>
|
|
||||||
* <li>LTC 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 Litecoin receiving address into public key hash (we only support P2PKH at this time)
|
|
||||||
Address litecoinReceivingAddress;
|
|
||||||
try {
|
|
||||||
litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
|
||||||
} catch (AddressFormatException e) {
|
|
||||||
throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
|
||||||
}
|
|
||||||
if (litecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
|
||||||
throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
|
||||||
|
|
||||||
byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.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/LTC ACCT";
|
|
||||||
String description = "QORT/LTC cross-chain trade";
|
|
||||||
String aTType = "ACCT";
|
|
||||||
String tags = "ACCT QORT LTC";
|
|
||||||
byte[] creationBytes = LitecoinACCTv2.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, LitecoinACCTv2.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.LITECOIN.name(),
|
|
||||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
|
||||||
tradeBotCreateRequest.foreignAmount, null, null, null, litecoinReceivingAccountInfo);
|
|
||||||
|
|
||||||
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 LTC to an existing offer.
|
|
||||||
* <p>
|
|
||||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
|
||||||
* and access to a Litecoin 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 Litecoin 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 Litecoin main-net)
|
|
||||||
* or 'tprv' for (Litecoin 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 Litecoin amount expected by 'Bob'.
|
|
||||||
* <p>
|
|
||||||
* If the Litecoin 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 Litecoin 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, LitecoinACCTv2.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.LITECOIN.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 = Litecoin.getInstance().getP2shFee(now);
|
|
||||||
} catch (ForeignBlockchainException e) {
|
|
||||||
LOGGER.debug("Couldn't estimate Litecoin 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 = Litecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
|
||||||
|
|
||||||
// Build transaction for funding P2SH-A
|
|
||||||
Transaction p2shFundingTransaction = Litecoin.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 {
|
|
||||||
Litecoin.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 = LitecoinACCTv2.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 = LitecoinACCTv2.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 Litecoin 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.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 Litecoin pubkeyhash and lockTime-A
|
|
||||||
byte[] messageData = messageTransactionData.getData();
|
|
||||||
LitecoinACCTv2.OfferMessageData offerMessageData = LitecoinACCTv2.extractOfferMessageData(messageData);
|
|
||||||
if (offerMessageData == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerLitecoinPKH;
|
|
||||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
|
||||||
int lockTimeA = (int) offerMessageData.lockTimeA;
|
|
||||||
long messageTimestamp = messageTransactionData.getTimestamp();
|
|
||||||
int refundTimeout = LitecoinACCTv2.calcRefundTimeout(messageTimestamp, lockTimeA);
|
|
||||||
|
|
||||||
// Determine P2SH-A address and confirm funded
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
|
||||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
|
||||||
|
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
|
||||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
|
||||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.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 = LitecoinACCTv2.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 LTC 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;
|
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.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 = litecoin.deriveP2shAddress(redeemScriptA);
|
|
||||||
|
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
|
||||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.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 = LitecoinACCTv2.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 = LitecoinACCTv2.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 LTC 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 LTC funds from P2SH-A
|
|
||||||
* to Bob's 'foreign'/Litecoin trade legacy-format address, as derived from trade private key.
|
|
||||||
* <p>
|
|
||||||
* (This could potentially be 'improved' to send LTC 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 LTC
|
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
|
||||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] secretA = LitecoinACCTv2.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
|
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
|
||||||
|
|
||||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
|
||||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
|
||||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
|
||||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.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 = litecoin.getUnspentOutputs(p2shAddressA);
|
|
||||||
|
|
||||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
|
||||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
|
||||||
|
|
||||||
litecoin.broadcastTransaction(p2shRedeemTransaction);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String receivingAddress = litecoin.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;
|
|
||||||
|
|
||||||
Litecoin litecoin = Litecoin.getInstance();
|
|
||||||
|
|
||||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
|
||||||
int medianBlockTime = litecoin.getMedianBlockTime();
|
|
||||||
if (medianBlockTime <= lockTimeA)
|
|
||||||
return;
|
|
||||||
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
|
||||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
|
||||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.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 = litecoin.getUnspentOutputs(p2shAddressA);
|
|
||||||
|
|
||||||
// Determine receive address for refund
|
|
||||||
String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
|
||||||
Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
|
|
||||||
|
|
||||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
|
|
||||||
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
|
||||||
|
|
||||||
litecoin.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
|||||||
import org.qortal.group.Group;
|
import org.qortal.group.Group;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.transaction.DeployAtTransaction;
|
import org.qortal.transaction.DeployAtTransaction;
|
||||||
import org.qortal.transaction.MessageTransaction;
|
import org.qortal.transaction.MessageTransaction;
|
||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
@ -317,20 +318,27 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
|||||||
|
|
||||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||||
if (!isMessageAlreadySent) {
|
if (!isMessageAlreadySent) {
|
||||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
|
||||||
|
new Thread(() -> {
|
||||||
|
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||||
|
|
||||||
messageTransaction.computeNonce();
|
messageTransaction.computeNonce();
|
||||||
messageTransaction.sign(sender);
|
messageTransaction.sign(sender);
|
||||||
|
|
||||||
// reset repository state to prevent deadlock
|
// reset repository state to prevent deadlock
|
||||||
repository.discardChanges();
|
threadsRepository.discardChanges();
|
||||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
if (result != ValidationResult.OK) {
|
if (result != ValidationResult.OK) {
|
||||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||||
return ResponseResult.NETWORK_ISSUE;
|
}
|
||||||
}
|
} catch (DataException e) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||||
|
}
|
||||||
|
}, "TradeBot response").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
package org.qortal.controller.tradebot;
|
package org.qortal.controller.tradebot;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.rust.litewalletjni.LiteWalletJni;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bitcoinj.core.*;
|
import org.bitcoinj.core.*;
|
||||||
import org.bitcoinj.script.Script.ScriptType;
|
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||||
@ -45,9 +46,9 @@ import static java.util.stream.Collectors.toMap;
|
|||||||
* <li>Trade-bot entries</li>
|
* <li>Trade-bot entries</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2TradeBot.class);
|
private static final Logger LOGGER = LogManager.getLogger(PirateChainACCTv3TradeBot.class);
|
||||||
|
|
||||||
public enum State implements TradeBot.StateNameAndValueSupplier {
|
public enum State implements TradeBot.StateNameAndValueSupplier {
|
||||||
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
|
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
|
||||||
@ -91,18 +92,18 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
/** 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 final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||||
|
|
||||||
private static DogecoinACCTv2TradeBot instance;
|
private static PirateChainACCTv3TradeBot instance;
|
||||||
|
|
||||||
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
|
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)
|
.map(State::name)
|
||||||
.collect(Collectors.toUnmodifiableList());
|
.collect(Collectors.toUnmodifiableList());
|
||||||
|
|
||||||
private DogecoinACCTv2TradeBot() {
|
private PirateChainACCTv3TradeBot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized DogecoinACCTv2TradeBot getInstance() {
|
public static synchronized PirateChainACCTv3TradeBot getInstance() {
|
||||||
if (instance == null)
|
if (instance == null)
|
||||||
instance = new DogecoinACCTv2TradeBot();
|
instance = new PirateChainACCTv3TradeBot();
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@ -113,7 +114,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for DOGE.
|
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for ARRR.
|
||||||
* <p>
|
* <p>
|
||||||
* Generates:
|
* Generates:
|
||||||
* <ul>
|
* <ul>
|
||||||
@ -122,14 +123,14 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
* Derives:
|
* Derives:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
|
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
|
||||||
* <li>'foreign' (as in Dogecoin) public key, public key hash</li>
|
* <li>'foreign' (as in PirateChain) public key, public key hash</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* A Qortal AT is then constructed including the following as constants in the 'data segment':
|
* A Qortal AT is then constructed including the following as constants in the 'data segment':
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
|
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
|
||||||
* <li>'foreign'/Dogecoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
* <li>'foreign'/PirateChain public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||||
* <li>QORT amount on offer by Bob</li>
|
* <li>QORT amount on offer by Bob</li>
|
||||||
* <li>DOGE amount expected in return by Bob (from Alice)</li>
|
* <li>ARRR amount expected in return by Bob (from Alice)</li>
|
||||||
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
|
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
|
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
|
||||||
@ -151,17 +152,18 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||||
|
|
||||||
// Convert Dogecoin receiving address into public key hash (we only support P2PKH at this time)
|
// ARRR wallet must be loaded before a trade can be created
|
||||||
Address dogecoinReceivingAddress;
|
// This is to stop trades from nodes on unsupported architectures (e.g. 32bit)
|
||||||
try {
|
if (!LiteWalletJni.isLoaded()) {
|
||||||
dogecoinReceivingAddress = Address.fromString(Dogecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
throw new DataException("Pirate wallet not found. Check wallets screen for details.");
|
||||||
} catch (AddressFormatException e) {
|
|
||||||
throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
|
||||||
}
|
}
|
||||||
if (dogecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
|
||||||
throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
|
||||||
|
|
||||||
byte[] dogecoinReceivingAccountInfo = dogecoinReceivingAddress.getHash();
|
if (!PirateChain.getInstance().isValidAddress(tradeBotCreateRequest.receivingAddress)) {
|
||||||
|
throw new DataException("Unsupported Pirate Chain receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bech32.Bech32Data decodedReceivingAddress = Bech32.decode(tradeBotCreateRequest.receivingAddress);
|
||||||
|
byte[] pirateChainReceivingAccountInfo = decodedReceivingAddress.data;
|
||||||
|
|
||||||
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
|
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
|
||||||
|
|
||||||
@ -172,11 +174,11 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
byte[] signature = null;
|
byte[] signature = null;
|
||||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
|
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
|
||||||
|
|
||||||
String name = "QORT/DOGE ACCT";
|
String name = "QORT/ARRR ACCT";
|
||||||
String description = "QORT/DOGE cross-chain trade";
|
String description = "QORT/ARRR cross-chain trade";
|
||||||
String aTType = "ACCT";
|
String aTType = "ACCT";
|
||||||
String tags = "ACCT QORT DOGE";
|
String tags = "ACCT QORT ARRR";
|
||||||
byte[] creationBytes = DogecoinACCTv2.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
|
byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKey, tradeBotCreateRequest.qortAmount,
|
||||||
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
|
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
|
||||||
long amount = tradeBotCreateRequest.fundingQortAmount;
|
long amount = tradeBotCreateRequest.fundingQortAmount;
|
||||||
|
|
||||||
@ -189,14 +191,14 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
|
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
|
||||||
String atAddress = deployAtTransactionData.getAtAddress();
|
String atAddress = deployAtTransactionData.getAtAddress();
|
||||||
|
|
||||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv2.NAME,
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, PirateChainACCTv3.NAME,
|
||||||
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
|
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
|
||||||
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
|
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
|
||||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||||
null, null,
|
null, null,
|
||||||
SupportedBlockchain.DOGECOIN.name(),
|
SupportedBlockchain.PIRATECHAIN.name(),
|
||||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
tradeBotCreateRequest.foreignAmount, null, null, null, dogecoinReceivingAccountInfo);
|
tradeBotCreateRequest.foreignAmount, null, null, null, pirateChainReceivingAccountInfo);
|
||||||
|
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
|
||||||
|
|
||||||
@ -212,15 +214,15 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching DOGE to an existing offer.
|
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching ARRR to an existing offer.
|
||||||
* <p>
|
* <p>
|
||||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||||
* and access to a Dogecoin wallet via <tt>xprv58</tt>.
|
* and access to a PirateChain wallet via <tt>xprv58</tt>.
|
||||||
* <p>
|
* <p>
|
||||||
* The <tt>crossChainTradeData</tt> contains the current trade offer state
|
* The <tt>crossChainTradeData</tt> contains the current trade offer state
|
||||||
* as extracted from the AT's data segment.
|
* as extracted from the AT's data segment.
|
||||||
* <p>
|
* <p>
|
||||||
* Access to a funded wallet is via a Dogecoin BIP32 hierarchical deterministic key,
|
* Access to a funded wallet is via a PirateChain BIP32 hierarchical deterministic key,
|
||||||
* passed via <tt>xprv58</tt>.
|
* passed via <tt>xprv58</tt>.
|
||||||
* <b>This key will be stored in your node's database</b>
|
* <b>This key will be stored in your node's database</b>
|
||||||
* to allow trade-bot to create/fund the necessary P2SH transactions!
|
* to allow trade-bot to create/fund the necessary P2SH transactions!
|
||||||
@ -230,26 +232,26 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
|
* 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>
|
* Electrum wallet by going to the console tab and entering:<br>
|
||||||
* <tt>wallet.keystore.xprv</tt><br>
|
* <tt>wallet.keystore.xprv</tt><br>
|
||||||
* which should result in a base58 string starting with either 'xprv' (for Dogecoin main-net)
|
* which should result in a base58 string starting with either 'xprv' (for PirateChain main-net)
|
||||||
* or 'tprv' for (Dogecoin test-net).
|
* or 'tprv' for (PirateChain test-net).
|
||||||
* <p>
|
* <p>
|
||||||
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
|
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
|
||||||
* <p>
|
* <p>
|
||||||
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
|
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
|
||||||
* with the Dogecoin amount expected by 'Bob'.
|
* with the PirateChain amount expected by 'Bob'.
|
||||||
* <p>
|
* <p>
|
||||||
* If the Dogecoin transaction is successfully broadcast to the network then
|
* If the PirateChain transaction is successfully broadcast to the network then
|
||||||
* we also send a MESSAGE to Bob's trade-bot to let them know.
|
* we also send a MESSAGE to Bob's trade-bot to let them know.
|
||||||
* <p>
|
* <p>
|
||||||
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
|
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
|
||||||
* <p>
|
* <p>
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
|
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
|
||||||
* @param xprv58 funded wallet xprv in base58
|
* @param seed58 funded wallet xprv in base58
|
||||||
* @return true if P2SH-A funding transaction successfully broadcast to Dogecoin network, false otherwise
|
* @return true if P2SH-A funding transaction successfully broadcast to PirateChain network, false otherwise
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
*/
|
*/
|
||||||
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
|
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String seed58, String receivingAddress) throws DataException {
|
||||||
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||||
byte[] secretA = TradeBot.generateSecret();
|
byte[] secretA = TradeBot.generateSecret();
|
||||||
byte[] hashOfSecretA = Crypto.hash160(secretA);
|
byte[] hashOfSecretA = Crypto.hash160(secretA);
|
||||||
@ -262,18 +264,22 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||||
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
||||||
|
|
||||||
|
String tradePrivateKey58 = Base58.encode(tradePrivateKey);
|
||||||
|
String tradeForeignPublicKey58 = Base58.encode(tradeForeignPublicKey);
|
||||||
|
String secret58 = Base58.encode(secretA);
|
||||||
|
|
||||||
// We need to generate lockTime-A: add tradeTimeout to now
|
// We need to generate lockTime-A: add tradeTimeout to now
|
||||||
long now = NTP.getTime();
|
long now = NTP.getTime();
|
||||||
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
|
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
|
||||||
|
|
||||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv2.NAME,
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, PirateChainACCTv3.NAME,
|
||||||
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
|
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
|
||||||
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
|
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
|
||||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||||
secretA, hashOfSecretA,
|
secretA, hashOfSecretA,
|
||||||
SupportedBlockchain.DOGECOIN.name(),
|
SupportedBlockchain.PIRATECHAIN.name(),
|
||||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
crossChainTradeData.expectedForeignAmount, seed58, null, lockTimeA, receivingPublicKeyHash);
|
||||||
|
|
||||||
// Attempt to backup the trade bot data
|
// Attempt to backup the trade bot data
|
||||||
// Include tradeBotData as an additional parameter, since it's not in the repository yet
|
// Include tradeBotData as an additional parameter, since it's not in the repository yet
|
||||||
@ -282,9 +288,9 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||||
long p2shFee;
|
long p2shFee;
|
||||||
try {
|
try {
|
||||||
p2shFee = Dogecoin.getInstance().getP2shFee(now);
|
p2shFee = PirateChain.getInstance().getP2shFee(now);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
LOGGER.debug("Couldn't estimate Dogecoin fees?");
|
LOGGER.debug("Couldn't estimate PirateChain fees?");
|
||||||
return ResponseResult.NETWORK_ISSUE;
|
return ResponseResult.NETWORK_ISSUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,26 +299,23 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
|
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
|
||||||
|
|
||||||
// P2SH-A to be funded
|
// P2SH-A to be funded
|
||||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
byte[] redeemScriptBytes = PirateChainHTLC.buildScript(tradeForeignPublicKey, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||||
String p2shAddress = Dogecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
String p2shAddressT3 = PirateChain.getInstance().deriveP2shAddress(redeemScriptBytes); // Use t3 prefix when funding
|
||||||
|
byte[] redeemScriptWithPrefixBytes = PirateChainHTLC.buildScriptWithPrefix(tradeForeignPublicKey, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||||
|
String redeemScriptWithPrefix58 = Base58.encode(redeemScriptWithPrefixBytes);
|
||||||
|
|
||||||
// Build transaction for funding P2SH-A
|
// Send to P2SH address
|
||||||
Transaction p2shFundingTransaction = Dogecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
try {
|
||||||
if (p2shFundingTransaction == null) {
|
String txid = PirateChain.getInstance().fundP2SH(seed58, p2shAddressT3, amountA, redeemScriptWithPrefix58);
|
||||||
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
|
LOGGER.info("fundingTxidHex: {}", txid);
|
||||||
|
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
LOGGER.debug("Unable to build and send P2SH-A funding transaction - lack of funds?");
|
||||||
return ResponseResult.BALANCE_ISSUE;
|
return ResponseResult.BALANCE_ISSUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
Dogecoin.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
|
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||||
byte[] messageData = DogecoinACCTv2.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
byte[] messageData = PirateChainACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKey(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||||
|
|
||||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||||
@ -333,11 +336,21 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddressT3));
|
||||||
|
|
||||||
return ResponseResult.OK;
|
return ResponseResult.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String hex(byte[] bytes) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (byte aByte : bytes) {
|
||||||
|
result.append(String.format("%02x", aByte));
|
||||||
|
// upper case
|
||||||
|
// result.append(String.format("%02X", aByte));
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
|
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||||
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||||
@ -354,6 +367,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
case BOB_DONE:
|
case BOB_DONE:
|
||||||
case ALICE_REFUNDED:
|
case ALICE_REFUNDED:
|
||||||
case BOB_REFUNDED:
|
case BOB_REFUNDED:
|
||||||
|
case ALICE_REFUNDING_A:
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -381,7 +395,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tradeBotState.requiresTradeData) {
|
if (tradeBotState.requiresTradeData) {
|
||||||
tradeData = DogecoinACCTv2.getInstance().populateTradeData(repository, atData);
|
tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
|
||||||
if (tradeData == null) {
|
if (tradeData == null) {
|
||||||
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
|
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
|
||||||
return;
|
return;
|
||||||
@ -462,7 +476,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
* <p>
|
* <p>
|
||||||
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
|
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
|
||||||
* <p>
|
* <p>
|
||||||
* Assuming P2SH-A has at least expected Dogecoin balance,
|
* Assuming P2SH-A has at least expected PirateChain balance,
|
||||||
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
|
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
|
||||||
* <p>
|
* <p>
|
||||||
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
|
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
|
||||||
@ -480,7 +494,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
String address = tradeBotData.getTradeNativeAddress();
|
String address = tradeBotData.getTradeNativeAddress();
|
||||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
|
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
|
||||||
@ -489,27 +503,27 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
if (messageTransactionData.isText())
|
if (messageTransactionData.isText())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// We're expecting: HASH160(secret-A), Alice's Dogecoin pubkeyhash and lockTime-A
|
// We're expecting: HASH160(secret-A), Alice's PirateChain pubkeyhash and lockTime-A
|
||||||
byte[] messageData = messageTransactionData.getData();
|
byte[] messageData = messageTransactionData.getData();
|
||||||
DogecoinACCTv2.OfferMessageData offerMessageData = DogecoinACCTv2.extractOfferMessageData(messageData);
|
PirateChainACCTv3.OfferMessageData offerMessageData = PirateChainACCTv3.extractOfferMessageData(messageData);
|
||||||
if (offerMessageData == null)
|
if (offerMessageData == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDogecoinPKH;
|
byte[] aliceForeignPublicKey = offerMessageData.partnerPirateChainPublicKey;
|
||||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||||
int lockTimeA = (int) offerMessageData.lockTimeA;
|
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||||
long messageTimestamp = messageTransactionData.getTimestamp();
|
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||||
int refundTimeout = DogecoinACCTv2.calcRefundTimeout(messageTimestamp, lockTimeA);
|
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||||
|
|
||||||
// Determine P2SH-A address and confirm funded
|
// Determine P2SH-A address and confirm funded
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
byte[] redeemScriptA = PirateChainHTLC.buildScript(aliceForeignPublicKey, lockTimeA, tradeBotData.getTradeForeignPublicKey(), hashOfSecretA);
|
||||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
|
||||||
|
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
@ -521,7 +535,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
case REDEEMED:
|
case REDEEMED:
|
||||||
// We've already redeemed this?
|
// We've already redeemed this?
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||||
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
|
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddress));
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case REFUND_IN_PROGRESS:
|
case REFUND_IN_PROGRESS:
|
||||||
@ -539,7 +553,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
|
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
|
||||||
|
|
||||||
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
||||||
byte[] outgoingMessageData = DogecoinACCTv2.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
byte[] outgoingMessageData = PirateChainACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
|
||||||
String messageRecipient = tradeBotData.getAtAddress();
|
String messageRecipient = tradeBotData.getAtAddress();
|
||||||
|
|
||||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
|
||||||
@ -578,7 +592,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
* <p>
|
* <p>
|
||||||
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
|
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
|
||||||
* <p>
|
* <p>
|
||||||
* In revealing a valid secret-A, Bob can then redeem the DOGE funds from P2SH-A.
|
* In revealing a valid secret-A, Bob can then redeem the ARRR funds from P2SH-A.
|
||||||
* <p>
|
* <p>
|
||||||
* @throws ForeignBlockchainException
|
* @throws ForeignBlockchainException
|
||||||
*/
|
*/
|
||||||
@ -587,19 +601,19 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
|
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
int lockTimeA = tradeBotData.getLockTimeA();
|
int lockTimeA = tradeBotData.getLockTimeA();
|
||||||
|
|
||||||
// Refund P2SH-A if we've passed lockTime-A
|
// Refund P2SH-A if we've passed lockTime-A
|
||||||
if (NTP.getTime() >= lockTimeA * 1000L) {
|
if (NTP.getTime() >= lockTimeA * 1000L) {
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
byte[] redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
|
||||||
|
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
|
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
@ -611,21 +625,21 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
case REDEEMED:
|
case REDEEMED:
|
||||||
// Already redeemed?
|
// Already redeemed?
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
|
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddress));
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case REFUND_IN_PROGRESS:
|
case REFUND_IN_PROGRESS:
|
||||||
case REFUNDED:
|
case REFUNDED:
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||||
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
|
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddress));
|
||||||
return;
|
return;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||||
() -> atData.getIsFinished()
|
() -> atData.getIsFinished()
|
||||||
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
|
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddress)
|
||||||
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
|
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddress));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -645,7 +659,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
||||||
int refundTimeout = DogecoinACCTv2.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
|
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
|
||||||
|
|
||||||
// Our calculated refundTimeout should match AT's refundTimeout
|
// Our calculated refundTimeout should match AT's refundTimeout
|
||||||
if (refundTimeout != crossChainTradeData.refundTimeout) {
|
if (refundTimeout != crossChainTradeData.refundTimeout) {
|
||||||
@ -659,7 +673,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
// Send 'redeem' MESSAGE to AT using both secret
|
// Send 'redeem' MESSAGE to AT using both secret
|
||||||
byte[] secretA = tradeBotData.getSecret();
|
byte[] secretA = tradeBotData.getSecret();
|
||||||
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
|
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
|
||||||
byte[] messageData = DogecoinACCTv2.buildRedeemMessage(secretA, qortalReceivingAddress);
|
byte[] messageData = PirateChainACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
|
||||||
String messageRecipient = tradeBotData.getAtAddress();
|
String messageRecipient = tradeBotData.getAtAddress();
|
||||||
|
|
||||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||||
@ -686,15 +700,15 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the DOGE funds from P2SH-A.
|
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the ARRR funds from P2SH-A.
|
||||||
* <p>
|
* <p>
|
||||||
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
|
* 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.
|
* trade-bot is done with this specific trade and finalizes in refunded state.
|
||||||
* <p>
|
* <p>
|
||||||
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the DOGE funds from P2SH-A
|
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the ARRR funds from P2SH-A
|
||||||
* to Bob's 'foreign'/Dogecoin trade legacy-format address, as derived from trade private key.
|
* to Bob's 'foreign'/PirateChain trade legacy-format address, as derived from trade private key.
|
||||||
* <p>
|
* <p>
|
||||||
* (This could potentially be 'improved' to send DOGE to any address of Bob's choosing by changing the transaction output).
|
* (This could potentially be 'improved' to send ARRR to any address of Bob's choosing by changing the transaction output).
|
||||||
* <p>
|
* <p>
|
||||||
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
|
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
|
||||||
* @throws ForeignBlockchainException
|
* @throws ForeignBlockchainException
|
||||||
@ -708,14 +722,14 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
|
|
||||||
// If AT is REFUNDED or CANCELLED then something has gone wrong
|
// If AT is REFUNDED or CANCELLED then something has gone wrong
|
||||||
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
|
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 DOGE
|
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the ARRR
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] secretA = DogecoinACCTv2.getInstance().findSecretA(repository, crossChainTradeData);
|
byte[] secretA = PirateChainACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
|
||||||
if (secretA == null) {
|
if (secretA == null) {
|
||||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||||
return;
|
return;
|
||||||
@ -723,18 +737,21 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
|
|
||||||
// Use secret-A to redeem P2SH-A
|
// Use secret-A to redeem P2SH-A
|
||||||
|
|
||||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
byte[] redeemScriptA = PirateChainHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
|
||||||
|
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); // Use 't3' prefix when refunding
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
String receivingAddress = Bech32.encode("zs", receivingAccountInfo);
|
||||||
|
|
||||||
|
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
@ -753,20 +770,27 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
case FUNDED: {
|
case FUNDED: {
|
||||||
|
// Get funding txid
|
||||||
|
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||||
|
if (fundingTxidHex == null) {
|
||||||
|
throw new ForeignBlockchainException("Missing funding txid when redeeming P2SH");
|
||||||
|
}
|
||||||
|
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
|
||||||
|
|
||||||
|
// Redeem P2SH
|
||||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
byte[] privateKey = tradeBotData.getTradePrivateKey();
|
||||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
|
String secret58 = Base58.encode(secretA);
|
||||||
|
String privateKey58 = Base58.encode(privateKey);
|
||||||
|
String redeemScript58 = Base58.encode(redeemScriptA);
|
||||||
|
|
||||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
String txid = PirateChain.getInstance().redeemP2sh(p2shAddressT3, receivingAddress, redeemAmount.value,
|
||||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
redeemScript58, fundingTxid58, secret58, privateKey58);
|
||||||
|
LOGGER.info("Redeem txid: {}", txid);
|
||||||
dogecoin.broadcastTransaction(p2shRedeemTransaction);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String receivingAddress = dogecoin.pkhToAddress(receivingAccountInfo);
|
|
||||||
|
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||||
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
|
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
|
||||||
}
|
}
|
||||||
@ -783,21 +807,22 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
if (NTP.getTime() <= lockTimeA * 1000L)
|
if (NTP.getTime() <= lockTimeA * 1000L)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
PirateChain pirateChain = PirateChain.getInstance();
|
||||||
|
|
||||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||||
int medianBlockTime = dogecoin.getMedianBlockTime();
|
int medianBlockTime = pirateChain.getMedianBlockTime();
|
||||||
if (medianBlockTime <= lockTimeA)
|
if (medianBlockTime <= lockTimeA)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
byte[] redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
|
||||||
|
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); // Use 't3' prefix when refunding
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||||
|
|
||||||
switch (htlcStatusA) {
|
switch (htlcStatusA) {
|
||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
@ -809,7 +834,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
case REDEEMED:
|
case REDEEMED:
|
||||||
// Too late!
|
// Too late!
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||||
() -> String.format("P2SH-A %s already spent!", p2shAddressA));
|
() -> String.format("P2SH-A %s already spent!", p2shAddress));
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case REFUND_IN_PROGRESS:
|
case REFUND_IN_PROGRESS:
|
||||||
@ -817,24 +842,28 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case FUNDED:{
|
case FUNDED:{
|
||||||
|
// Get funding txid
|
||||||
|
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||||
|
if (fundingTxidHex == null) {
|
||||||
|
throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
|
||||||
|
}
|
||||||
|
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
|
||||||
|
|
||||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
byte[] privateKey = tradeBotData.getTradePrivateKey();
|
||||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
|
String privateKey58 = Base58.encode(privateKey);
|
||||||
|
String redeemScript58 = Base58.encode(redeemScriptA);
|
||||||
|
String receivingAddress = pirateChain.getWalletAddress(tradeBotData.getForeignKey());
|
||||||
|
|
||||||
// Determine receive address for refund
|
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
|
||||||
String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
receivingAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTimeA, privateKey58);
|
||||||
Address receiving = Address.fromString(dogecoin.getNetworkParameters(), receiveAddress);
|
LOGGER.info("Refund txid: {}", txid);
|
||||||
|
|
||||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(dogecoin.getNetworkParameters(), refundAmount, refundKey,
|
|
||||||
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
|
||||||
|
|
||||||
dogecoin.broadcastTransaction(p2shRefundTransaction);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||||
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
|
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -96,13 +96,12 @@ public class TradeBot implements Listener {
|
|||||||
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(BitcoinACCTv3.class, BitcoinACCTv3TradeBot::getInstance);
|
acctTradeBotSuppliers.put(BitcoinACCTv3.class, BitcoinACCTv3TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(LitecoinACCTv2.class, LitecoinACCTv2TradeBot::getInstance);
|
|
||||||
acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance);
|
acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
|
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
|
|
||||||
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
|
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
|
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
|
||||||
acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
|
acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
|
||||||
|
acctTradeBotSuppliers.put(PirateChainACCTv3.class, PirateChainACCTv3TradeBot::getInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TradeBot instance;
|
private static TradeBot instance;
|
||||||
@ -292,14 +291,14 @@ public class TradeBot implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] deriveTradeNativePublicKey(byte[] privateKey) {
|
public static byte[] deriveTradeNativePublicKey(byte[] privateKey) {
|
||||||
return PrivateKeyAccount.toPublicKey(privateKey);
|
return Crypto.toPublicKey(privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] deriveTradeForeignPublicKey(byte[] privateKey) {
|
public static byte[] deriveTradeForeignPublicKey(byte[] privateKey) {
|
||||||
return ECKey.fromPrivate(privateKey).getPubKey();
|
return ECKey.fromPrivate(privateKey).getPubKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*package*/ static byte[] generateSecret() {
|
/*package*/ public static byte[] generateSecret() {
|
||||||
byte[] secret = new byte[32];
|
byte[] secret = new byte[32];
|
||||||
RANDOM.nextBytes(secret);
|
RANDOM.nextBytes(secret);
|
||||||
return secret;
|
return secret;
|
||||||
@ -469,9 +468,6 @@ public class TradeBot implements Listener {
|
|||||||
|
|
||||||
List<TradePresenceData> safeTradePresences = List.copyOf(this.safeAllTradePresencesByPubkey.values());
|
List<TradePresenceData> safeTradePresences = List.copyOf(this.safeAllTradePresencesByPubkey.values());
|
||||||
|
|
||||||
if (safeTradePresences.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
LOGGER.debug("Broadcasting all {} known trade presences. Next broadcast timestamp: {}",
|
LOGGER.debug("Broadcasting all {} known trade presences. Next broadcast timestamp: {}",
|
||||||
safeTradePresences.size(), nextTradePresenceBroadcastTimestamp
|
safeTradePresences.size(), nextTradePresenceBroadcastTimestamp
|
||||||
);
|
);
|
||||||
@ -638,7 +634,7 @@ public class TradeBot implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newCount > 0) {
|
if (newCount > 0) {
|
||||||
LOGGER.debug("New trade presences: {}", newCount);
|
LOGGER.debug("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
|
||||||
rebuildSafeAllTradePresences();
|
rebuildSafeAllTradePresences();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,8 @@ public class Bitcoin extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(bitcoinNet.getParams());
|
Context bitcoinjContext = new Context(bitcoinNet.getParams());
|
||||||
|
|
||||||
instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -29,6 +29,7 @@ import org.bitcoinj.wallet.SendRequest;
|
|||||||
import org.bitcoinj.wallet.Wallet;
|
import org.bitcoinj.wallet.Wallet;
|
||||||
import org.qortal.api.model.SimpleForeignTransaction;
|
import org.qortal.api.model.SimpleForeignTransaction;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.utils.Amounts;
|
import org.qortal.utils.Amounts;
|
||||||
import org.qortal.utils.BitTwiddling;
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
public static final int HASH160_LENGTH = 20;
|
public static final int HASH160_LENGTH = 20;
|
||||||
|
|
||||||
protected final BitcoinyBlockchainProvider blockchain;
|
protected final BitcoinyBlockchainProvider blockchainProvider;
|
||||||
protected final Context bitcoinjContext;
|
protected final Context bitcoinjContext;
|
||||||
protected final String currencyCode;
|
protected final String currencyCode;
|
||||||
|
|
||||||
@ -61,18 +62,13 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
/** How many wallet keys to generate in each batch. */
|
/** How many wallet keys to generate in each batch. */
|
||||||
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3;
|
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3;
|
||||||
|
|
||||||
/** How many wallet keys to generate when using bitcoinj as the data provider.
|
|
||||||
* We must use a higher value here since we are unable to request multiple batches of keys.
|
|
||||||
* Without this, the bitcoinj state can be missing transactions, causing errors such as "insufficient balance". */
|
|
||||||
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ = 50;
|
|
||||||
|
|
||||||
/** Byte offset into raw block headers to block timestamp. */
|
/** Byte offset into raw block headers to block timestamp. */
|
||||||
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||||
|
|
||||||
// Constructors and instance
|
// Constructors and instance
|
||||||
|
|
||||||
protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) {
|
||||||
this.blockchain = blockchain;
|
this.blockchainProvider = blockchainProvider;
|
||||||
this.bitcoinjContext = bitcoinjContext;
|
this.bitcoinjContext = bitcoinjContext;
|
||||||
this.currencyCode = currencyCode;
|
this.currencyCode = currencyCode;
|
||||||
|
|
||||||
@ -82,7 +78,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
// Getters & setters
|
// Getters & setters
|
||||||
|
|
||||||
public BitcoinyBlockchainProvider getBlockchainProvider() {
|
public BitcoinyBlockchainProvider getBlockchainProvider() {
|
||||||
return this.blockchain;
|
return this.blockchainProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context getBitcoinjContext() {
|
public Context getBitcoinjContext() {
|
||||||
@ -155,10 +151,10 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if error occurs
|
* @throws ForeignBlockchainException if error occurs
|
||||||
*/
|
*/
|
||||||
public int getMedianBlockTime() throws ForeignBlockchainException {
|
public int getMedianBlockTime() throws ForeignBlockchainException {
|
||||||
int height = this.blockchain.getCurrentHeight();
|
int height = this.blockchainProvider.getCurrentHeight();
|
||||||
|
|
||||||
// Grab latest 11 blocks
|
// Grab latest 11 blocks
|
||||||
List<byte[]> blockHeaders = this.blockchain.getRawBlockHeaders(height - 11, 11);
|
List<byte[]> blockHeaders = this.blockchainProvider.getRawBlockHeaders(height - 11, 11);
|
||||||
if (blockHeaders.size() < 11)
|
if (blockHeaders.size() < 11)
|
||||||
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
|
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
|
||||||
|
|
||||||
@ -197,7 +193,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if there was an error
|
* @throws ForeignBlockchainException if there was an error
|
||||||
*/
|
*/
|
||||||
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
|
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
|
||||||
return this.blockchain.getConfirmedBalance(addressToScriptPubKey(base58Address));
|
return this.blockchainProvider.getConfirmedBalance(addressToScriptPubKey(base58Address));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,7 +204,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
*/
|
*/
|
||||||
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
|
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
|
||||||
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
|
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
|
||||||
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
|
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
|
||||||
|
|
||||||
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||||
@ -228,7 +224,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
*/
|
*/
|
||||||
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
|
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
|
||||||
public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
|
public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
|
||||||
byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash);
|
byte[] rawTransactionBytes = this.blockchainProvider.getRawTransaction(txHash);
|
||||||
|
|
||||||
Context.propagate(bitcoinjContext);
|
Context.propagate(bitcoinjContext);
|
||||||
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
|
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
|
||||||
@ -245,7 +241,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
ForeignBlockchainException e2 = null;
|
ForeignBlockchainException e2 = null;
|
||||||
while (retries <= 3) {
|
while (retries <= 3) {
|
||||||
try {
|
try {
|
||||||
return this.blockchain.getAddressTransactions(scriptPubKey, includeUnconfirmed);
|
return this.blockchainProvider.getAddressTransactions(scriptPubKey, includeUnconfirmed);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
e2 = e;
|
e2 = e;
|
||||||
retries++;
|
retries++;
|
||||||
@ -261,7 +257,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if there was an error.
|
* @throws ForeignBlockchainException if there was an error.
|
||||||
*/
|
*/
|
||||||
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||||
return this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
|
return this.blockchainProvider.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,11 +266,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if there was an error
|
* @throws ForeignBlockchainException if there was an error
|
||||||
*/
|
*/
|
||||||
public List<byte[]> getAddressTransactions(String base58Address) throws ForeignBlockchainException {
|
public List<byte[]> getAddressTransactions(String base58Address) throws ForeignBlockchainException {
|
||||||
List<TransactionHash> transactionHashes = this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), false);
|
List<TransactionHash> transactionHashes = this.blockchainProvider.getAddressTransactions(addressToScriptPubKey(base58Address), false);
|
||||||
|
|
||||||
List<byte[]> rawTransactions = new ArrayList<>();
|
List<byte[]> rawTransactions = new ArrayList<>();
|
||||||
for (TransactionHash transactionInfo : transactionHashes) {
|
for (TransactionHash transactionInfo : transactionHashes) {
|
||||||
byte[] rawTransaction = this.blockchain.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
|
byte[] rawTransaction = this.blockchainProvider.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
|
||||||
rawTransactions.add(rawTransaction);
|
rawTransactions.add(rawTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,7 +288,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
ForeignBlockchainException e2 = null;
|
ForeignBlockchainException e2 = null;
|
||||||
while (retries <= 3) {
|
while (retries <= 3) {
|
||||||
try {
|
try {
|
||||||
return this.blockchain.getTransaction(txHash);
|
return this.blockchainProvider.getTransaction(txHash);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
e2 = e;
|
e2 = e;
|
||||||
retries++;
|
retries++;
|
||||||
@ -307,7 +303,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @throws ForeignBlockchainException if error occurs
|
* @throws ForeignBlockchainException if error occurs
|
||||||
*/
|
*/
|
||||||
public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
|
public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
|
||||||
this.blockchain.broadcastTransaction(transaction.bitcoinSerialize());
|
this.blockchainProvider.broadcastTransaction(transaction.bitcoinSerialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -360,7 +356,24 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @param key58 BIP32/HD extended Bitcoin private/public key
|
* @param key58 BIP32/HD extended Bitcoin private/public key
|
||||||
* @return unspent BTC balance, or null if unable to determine balance
|
* @return unspent BTC balance, or null if unable to determine balance
|
||||||
*/
|
*/
|
||||||
public Long getWalletBalance(String key58) {
|
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
|
||||||
|
Long balance = 0L;
|
||||||
|
|
||||||
|
List<TransactionOutput> allUnspentOutputs = new ArrayList<>();
|
||||||
|
Set<String> walletAddresses = this.getWalletAddresses(key58);
|
||||||
|
for (String address : walletAddresses) {
|
||||||
|
allUnspentOutputs.addAll(this.getUnspentOutputs(address));
|
||||||
|
}
|
||||||
|
for (TransactionOutput output : allUnspentOutputs) {
|
||||||
|
if (!output.isAvailableForSpending()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
balance += output.getValue().value;
|
||||||
|
}
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getWalletBalanceFromBitcoinj(String key58) {
|
||||||
Context.propagate(bitcoinjContext);
|
Context.propagate(bitcoinjContext);
|
||||||
|
|
||||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||||
@ -375,7 +388,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
||||||
long balance = 0;
|
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());
|
List<SimpleTransaction> transactions = getWalletTransactions(key58).stream().sorted(oldestTimestampFirstComparator).collect(Collectors.toList());
|
||||||
for (SimpleTransaction transaction : transactions) {
|
for (SimpleTransaction transaction : transactions) {
|
||||||
balance += transaction.getTotalAmount();
|
balance += transaction.getTotalAmount();
|
||||||
@ -409,9 +422,6 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
||||||
Set<String> keySet = new HashSet<>();
|
Set<String> keySet = new HashSet<>();
|
||||||
|
|
||||||
// Set the number of consecutive empty batches required before giving up
|
|
||||||
final int numberOfAdditionalBatchesToSearch = 7;
|
|
||||||
|
|
||||||
int unusedCounter = 0;
|
int unusedCounter = 0;
|
||||||
int ki = 0;
|
int ki = 0;
|
||||||
do {
|
do {
|
||||||
@ -438,12 +448,12 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
if (areAllKeysUnused) {
|
if (areAllKeysUnused) {
|
||||||
// No transactions
|
// No transactions
|
||||||
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
|
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
|
||||||
// ... and we've hit our search limit
|
// ... and we've hit our search limit
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// We haven't hit our search limit yet so increment the counter and keep looking
|
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||||
unusedCounter++;
|
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
|
||||||
} else {
|
} else {
|
||||||
// Some keys in this batch were used, so reset the counter
|
// Some keys in this batch were used, so reset the counter
|
||||||
unusedCounter = 0;
|
unusedCounter = 0;
|
||||||
@ -455,7 +465,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
// Process new keys
|
// Process new keys
|
||||||
} while (true);
|
} while (true);
|
||||||
|
|
||||||
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
|
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingLong(SimpleTransaction::getTimestamp).reversed();
|
||||||
|
|
||||||
// Update cache and return
|
// Update cache and return
|
||||||
transactionsCacheTimestamp = NTP.getTime();
|
transactionsCacheTimestamp = NTP.getTime();
|
||||||
@ -468,6 +478,64 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getWalletAddresses(String key58) throws ForeignBlockchainException {
|
||||||
|
synchronized (this) {
|
||||||
|
Context.propagate(bitcoinjContext);
|
||||||
|
|
||||||
|
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||||
|
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||||
|
|
||||||
|
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
|
||||||
|
keyChain.maybeLookAhead();
|
||||||
|
|
||||||
|
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
||||||
|
|
||||||
|
Set<String> keySet = new HashSet<>();
|
||||||
|
|
||||||
|
int unusedCounter = 0;
|
||||||
|
int ki = 0;
|
||||||
|
do {
|
||||||
|
boolean areAllKeysUnused = true;
|
||||||
|
|
||||||
|
for (; ki < keys.size(); ++ki) {
|
||||||
|
DeterministicKey dKey = keys.get(ki);
|
||||||
|
|
||||||
|
// Check for transactions
|
||||||
|
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||||
|
keySet.add(address.toString());
|
||||||
|
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||||
|
|
||||||
|
// Ask for transaction history - if it's empty then key has never been used
|
||||||
|
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
|
||||||
|
|
||||||
|
if (!historicTransactionHashes.isEmpty()) {
|
||||||
|
areAllKeysUnused = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areAllKeysUnused) {
|
||||||
|
// No transactions
|
||||||
|
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
|
||||||
|
// ... and we've hit our search limit
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||||
|
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
|
||||||
|
} else {
|
||||||
|
// Some keys in this batch were used, so reset the counter
|
||||||
|
unusedCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate some more keys
|
||||||
|
keys.addAll(generateMoreKeys(keyChain));
|
||||||
|
|
||||||
|
// Process new keys
|
||||||
|
} while (true);
|
||||||
|
|
||||||
|
return keySet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
||||||
long amount = 0;
|
long amount = 0;
|
||||||
long total = 0L;
|
long total = 0L;
|
||||||
@ -537,7 +605,8 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
// All inputs and outputs relate to this wallet, so the balance should be unaffected
|
// All inputs and outputs relate to this wallet, so the balance should be unaffected
|
||||||
amount = 0;
|
amount = 0;
|
||||||
}
|
}
|
||||||
return new SimpleTransaction(t.txHash, t.timestamp, amount, fee, inputs, outputs);
|
long timestampMillis = t.timestamp * 1000L;
|
||||||
|
return new SimpleTransaction(t.txHash, timestampMillis, amount, fee, inputs, outputs, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -573,7 +642,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||||
|
|
||||||
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(script, false);
|
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(script, false);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If there are no unspent outputs then either:
|
* If there are no unspent outputs then either:
|
||||||
@ -591,7 +660,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ask for transaction history - if it's empty then key has never been used
|
// Ask for transaction history - if it's empty then key has never been used
|
||||||
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
|
List<TransactionHash> historicTransactionHashes = this.blockchainProvider.getAddressTransactions(script, false);
|
||||||
|
|
||||||
if (!historicTransactionHashes.isEmpty()) {
|
if (!historicTransactionHashes.isEmpty()) {
|
||||||
// Fully spent key - case (a)
|
// Fully spent key - case (a)
|
||||||
@ -629,7 +698,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
this.keyChain = this.wallet.getActiveKeyChain();
|
this.keyChain = this.wallet.getActiveKeyChain();
|
||||||
|
|
||||||
// Set up wallet's key chain
|
// Set up wallet's key chain
|
||||||
this.keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ);
|
this.keyChain.setLookaheadSize(Settings.getInstance().getBitcoinjLookaheadSize());
|
||||||
this.keyChain.maybeLookAhead();
|
this.keyChain.maybeLookAhead();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -650,7 +719,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
|
|
||||||
List<UnspentOutput> unspentOutputs;
|
List<UnspentOutput> unspentOutputs;
|
||||||
try {
|
try {
|
||||||
unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false);
|
unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
|
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
|
||||||
}
|
}
|
||||||
@ -674,7 +743,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
// Ask for transaction history - if it's empty then key has never been used
|
// Ask for transaction history - if it's empty then key has never been used
|
||||||
List<TransactionHash> historicTransactionHashes;
|
List<TransactionHash> historicTransactionHashes;
|
||||||
try {
|
try {
|
||||||
historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false);
|
historicTransactionHashes = this.bitcoiny.blockchainProvider.getAddressTransactions(script, false);
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
|
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
|
||||||
}
|
}
|
||||||
@ -727,7 +796,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
@Override
|
@Override
|
||||||
public int getChainHeadHeight() throws UTXOProviderException {
|
public int getChainHeadHeight() throws UTXOProviderException {
|
||||||
try {
|
try {
|
||||||
return this.bitcoiny.blockchain.getCurrentHeight();
|
return this.bitcoiny.blockchainProvider.getCurrentHeight();
|
||||||
} catch (ForeignBlockchainException e) {
|
} catch (ForeignBlockchainException e) {
|
||||||
throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
|
throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package org.qortal.crosschain;
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class BitcoinyBlockchainProvider {
|
public abstract class BitcoinyBlockchainProvider {
|
||||||
@ -7,18 +9,32 @@ public abstract class BitcoinyBlockchainProvider {
|
|||||||
public static final boolean INCLUDE_UNCONFIRMED = true;
|
public static final boolean INCLUDE_UNCONFIRMED = true;
|
||||||
public static final boolean EXCLUDE_UNCONFIRMED = false;
|
public static final boolean EXCLUDE_UNCONFIRMED = false;
|
||||||
|
|
||||||
|
/** Sets the blockchain using this provider instance */
|
||||||
|
public abstract void setBlockchain(Bitcoiny blockchain);
|
||||||
|
|
||||||
/** Returns ID unique to bitcoiny network (e.g. "Litecoin-TEST3") */
|
/** Returns ID unique to bitcoiny network (e.g. "Litecoin-TEST3") */
|
||||||
public abstract String getNetId();
|
public abstract String getNetId();
|
||||||
|
|
||||||
/** Returns current blockchain height. */
|
/** Returns current blockchain height. */
|
||||||
public abstract int getCurrentHeight() throws ForeignBlockchainException;
|
public abstract int getCurrentHeight() throws ForeignBlockchainException;
|
||||||
|
|
||||||
|
/** Returns a list of compact blocks, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max.
|
||||||
|
* Used for Pirate/Zcash only. If ever needed for other blockchains, the response format will need to be
|
||||||
|
* made generic. */
|
||||||
|
public abstract List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException;
|
||||||
|
|
||||||
/** Returns a list of raw block headers, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
|
/** Returns a list of raw block headers, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
|
||||||
public abstract List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException;
|
public abstract List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException;
|
||||||
|
|
||||||
|
/** Returns a list of block timestamps, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
|
||||||
|
public abstract List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException;
|
||||||
|
|
||||||
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
|
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
|
||||||
public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException;
|
public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException;
|
||||||
|
|
||||||
|
/** Returns balance of base58 encoded address. */
|
||||||
|
public abstract long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException;
|
||||||
|
|
||||||
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
|
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
|
||||||
public abstract byte[] getRawTransaction(String txHash) throws ForeignBlockchainException;
|
public abstract byte[] getRawTransaction(String txHash) throws ForeignBlockchainException;
|
||||||
|
|
||||||
@ -31,6 +47,12 @@ public abstract class BitcoinyBlockchainProvider {
|
|||||||
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
|
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
|
||||||
public abstract List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
public abstract List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||||
|
|
||||||
|
/** Returns list of BitcoinyTransaction objects for <tt>address</tt>, optionally including unconfirmed transactions. */
|
||||||
|
public abstract List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||||
|
|
||||||
|
/** Returns list of unspent transaction outputs for <tt>address</tt>, optionally including unconfirmed transactions. */
|
||||||
|
public abstract List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||||
|
|
||||||
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
|
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
|
||||||
public abstract List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
public abstract List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.qortal.crosschain;
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -10,8 +11,13 @@ import javax.xml.bind.annotation.XmlTransient;
|
|||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
public class BitcoinyTransaction {
|
public class BitcoinyTransaction {
|
||||||
|
|
||||||
|
public static final Comparator<BitcoinyTransaction> CONFIRMED_FIRST = (a, b) -> Boolean.compare(a.height != 0, b.height != 0);
|
||||||
|
|
||||||
public final String txHash;
|
public final String txHash;
|
||||||
|
|
||||||
|
@XmlTransient
|
||||||
|
public Integer height;
|
||||||
|
|
||||||
@XmlTransient
|
@XmlTransient
|
||||||
public final int size;
|
public final int size;
|
||||||
|
|
||||||
@ -113,6 +119,10 @@ public class BitcoinyTransaction {
|
|||||||
this.totalAmount = outputs.stream().map(output -> output.value).reduce(0L, Long::sum);
|
this.totalAmount = outputs.stream().map(output -> output.value).reduce(0L, Long::sum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return this.height;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("txHash %s, size %d, locktime %d, timestamp %d\n"
|
return String.format("txHash %s, size %d, locktime %d, timestamp %d\n"
|
||||||
+ "\tinputs: [%s]\n"
|
+ "\tinputs: [%s]\n"
|
||||||
|
@ -134,6 +134,8 @@ public class Digibyte extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(digibyteNet.getParams());
|
Context bitcoinjContext = new Context(digibyteNet.getParams());
|
||||||
|
|
||||||
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -47,7 +47,8 @@ public class Dogecoin extends Bitcoiny {
|
|||||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20060),
|
new Server("electrum1.cipig.net", ConnectionType.SSL, 20060),
|
||||||
new Server("electrum2.cipig.net", ConnectionType.SSL, 20060),
|
new Server("electrum2.cipig.net", ConnectionType.SSL, 20060),
|
||||||
new Server("electrum3.cipig.net", ConnectionType.SSL, 20060));
|
new Server("electrum3.cipig.net", ConnectionType.SSL, 20060),
|
||||||
|
new Server("161.97.137.235", ConnectionType.SSL, 50002));
|
||||||
// TODO: add more mainnet servers. It's too centralized.
|
// TODO: add more mainnet servers. It's too centralized.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +136,8 @@ public class Dogecoin extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(dogecoinNet.getParams());
|
Context bitcoinjContext = new Context(dogecoinNet.getParams());
|
||||||
|
|
||||||
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -5,12 +5,14 @@ import java.math.BigDecimal;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.net.ssl.SSLSocketFactory;
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.json.simple.JSONArray;
|
import org.json.simple.JSONArray;
|
||||||
@ -29,7 +31,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
|
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
|
||||||
private static final Random RANDOM = new Random();
|
private static final Random RANDOM = new Random();
|
||||||
|
|
||||||
|
// See: https://electrumx.readthedocs.io/en/latest/protocol-changes.html
|
||||||
private static final double MIN_PROTOCOL_VERSION = 1.2;
|
private static final double MIN_PROTOCOL_VERSION = 1.2;
|
||||||
|
private static final double MAX_PROTOCOL_VERSION = 2.0; // Higher than current latest, for hopeful future-proofing
|
||||||
|
private static final String CLIENT_NAME = "Qortal";
|
||||||
|
|
||||||
private static final int BLOCK_HEADER_LENGTH = 80;
|
private static final int BLOCK_HEADER_LENGTH = 80;
|
||||||
|
|
||||||
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
|
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
|
||||||
@ -39,7 +45,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
||||||
|
|
||||||
private static final int RESPONSE_TIME_READINGS = 5;
|
private static final int RESPONSE_TIME_READINGS = 5;
|
||||||
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
|
private static final long MAX_AVG_RESPONSE_TIME = 1000L; // ms
|
||||||
|
|
||||||
public static class Server {
|
public static class Server {
|
||||||
String hostname;
|
String hostname;
|
||||||
@ -107,6 +113,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
private final String netId;
|
private final String netId;
|
||||||
private final String expectedGenesisHash;
|
private final String expectedGenesisHash;
|
||||||
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
|
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
|
||||||
|
private Bitcoiny blockchain;
|
||||||
|
|
||||||
private final Object serverLock = new Object();
|
private final Object serverLock = new Object();
|
||||||
private Server currentServer;
|
private Server currentServer;
|
||||||
@ -135,6 +142,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
|
|
||||||
// Methods for use by other classes
|
// Methods for use by other classes
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockchain(Bitcoiny blockchain) {
|
||||||
|
this.blockchain = blockchain;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getNetId() {
|
public String getNetId() {
|
||||||
return this.netId;
|
return this.netId;
|
||||||
@ -161,6 +173,16 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
return ((Long) heightObj).intValue();
|
return ((Long) heightObj).intValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of raw blocks, starting from <tt>startHeight</tt> inclusive.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
|
||||||
|
throw new ForeignBlockchainException("getCompactBlocks not implemented for ElectrumX due to being specific to zcash");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
|
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
|
||||||
* <p>
|
* <p>
|
||||||
@ -222,6 +244,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
return rawBlockHeaders;
|
return rawBlockHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of raw block timestamps, starting from <tt>startHeight</tt> inclusive.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException {
|
||||||
|
// FUTURE: implement this if needed. For now we use getRawBlockHeaders directly
|
||||||
|
throw new ForeignBlockchainException("getBlockTimestamps not yet implemented for ElectrumX");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns confirmed balance, based on passed payment script.
|
* Returns confirmed balance, based on passed payment script.
|
||||||
* <p>
|
* <p>
|
||||||
@ -247,6 +280,29 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
return (Long) balanceJson.get("confirmed");
|
return (Long) balanceJson.get("confirmed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns confirmed balance, based on passed base58 encoded address.
|
||||||
|
* <p>
|
||||||
|
* @return confirmed balance, or zero if address unknown
|
||||||
|
* @throws ForeignBlockchainException if there was an error
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException {
|
||||||
|
throw new ForeignBlockchainException("getConfirmedAddressBalance not yet implemented for ElectrumX");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of unspent outputs pertaining to passed address.
|
||||||
|
* <p>
|
||||||
|
* @return list of unspent outputs, or empty list if address unknown
|
||||||
|
* @throws ForeignBlockchainException if there was an error.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||||
|
byte[] script = this.blockchain.addressToScriptPubKey(address);
|
||||||
|
return this.getUnspentOutputs(script, includeUnconfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list of unspent outputs pertaining to passed payment script.
|
* Returns list of unspent outputs pertaining to passed payment script.
|
||||||
* <p>
|
* <p>
|
||||||
@ -482,6 +538,12 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
return transactionHashes;
|
return transactionHashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||||
|
// FUTURE: implement this if needed. For now we use getAddressTransactions() + getTransaction()
|
||||||
|
throw new ForeignBlockchainException("getAddressBitcoinyTransactions not yet implemented for ElectrumX");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts raw transaction to network.
|
* Broadcasts raw transaction to network.
|
||||||
* <p>
|
* <p>
|
||||||
@ -622,6 +684,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
this.scanner = new Scanner(this.socket.getInputStream());
|
this.scanner = new Scanner(this.socket.getInputStream());
|
||||||
this.scanner.useDelimiter("\n");
|
this.scanner.useDelimiter("\n");
|
||||||
|
|
||||||
|
// All connections need to start with a version negotiation
|
||||||
|
this.connectedRpc("server.version");
|
||||||
|
|
||||||
// Check connection is suitable by asking for server features, including genesis block hash
|
// Check connection is suitable by asking for server features, including genesis block hash
|
||||||
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
|
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
|
||||||
|
|
||||||
@ -668,6 +733,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
|
|
||||||
JSONArray requestParams = new JSONArray();
|
JSONArray requestParams = new JSONArray();
|
||||||
requestParams.addAll(Arrays.asList(params));
|
requestParams.addAll(Arrays.asList(params));
|
||||||
|
|
||||||
|
// server.version needs additional params to negotiate a version
|
||||||
|
if (method.equals("server.version")) {
|
||||||
|
requestParams.add(CLIENT_NAME);
|
||||||
|
List<String> versions = new ArrayList<>();
|
||||||
|
DecimalFormat df = new DecimalFormat("#.#");
|
||||||
|
versions.add(df.format(MIN_PROTOCOL_VERSION));
|
||||||
|
versions.add(df.format(MAX_PROTOCOL_VERSION));
|
||||||
|
requestParams.add(versions);
|
||||||
|
}
|
||||||
|
|
||||||
requestJson.put("params", requestParams);
|
requestJson.put("params", requestParams);
|
||||||
|
|
||||||
String request = requestJson.toJSONString() + "\n";
|
String request = requestJson.toJSONString() + "\n";
|
||||||
@ -682,6 +758,10 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
} catch (IOException | NoSuchElementException e) {
|
} catch (IOException | NoSuchElementException e) {
|
||||||
// Unable to send, or receive -- try another server?
|
// Unable to send, or receive -- try another server?
|
||||||
return null;
|
return null;
|
||||||
|
} catch (NoSuchMethodError e) {
|
||||||
|
// Likely an SSL dependency issue - retries are unlikely to succeed
|
||||||
|
LOGGER.error("ElectrumX output stream error", e);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
long endTime = System.currentTimeMillis();
|
long endTime = System.currentTimeMillis();
|
||||||
|
254
src/main/java/org/qortal/crosschain/LegacyZcashAddress.java
Normal file
254
src/main/java/org/qortal/crosschain/LegacyZcashAddress.java
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2011 Google Inc.
|
||||||
|
* Copyright 2014 Giannis Dzegoutanis
|
||||||
|
* Copyright 2015 Andreas Schildbach
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Updated for Zcash in May 2022 by Qortal core dev team. Modifications allow
|
||||||
|
* correct encoding of P2SH (t3) addresses only. */
|
||||||
|
|
||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.*;
|
||||||
|
import org.bitcoinj.params.Networks;
|
||||||
|
import org.bitcoinj.script.Script.ScriptType;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A Bitcoin address looks like 1MsScoe2fTJoq4ZPdQgqyhgWeoNamYPevy and is derived from an elliptic curve public key
|
||||||
|
* plus a set of network parameters. Not to be confused with a {@link PeerAddress} or {@link AddressMessage}
|
||||||
|
* which are about network (TCP) addresses.</p>
|
||||||
|
*
|
||||||
|
* <p>A standard address is built by taking the RIPE-MD160 hash of the public key bytes, with a version prefix and a
|
||||||
|
* checksum suffix, then encoding it textually as base58. The version prefix is used to both denote the network for
|
||||||
|
* which the address is valid (see {@link NetworkParameters}, and also to indicate how the bytes inside the address
|
||||||
|
* should be interpreted. Whilst almost all addresses today are hashes of public keys, another (currently unsupported
|
||||||
|
* type) can contain a hash of a script instead.</p>
|
||||||
|
*/
|
||||||
|
public class LegacyZcashAddress extends Address {
|
||||||
|
/**
|
||||||
|
* An address is a RIPEMD160 hash of a public key, therefore is always 160 bits or 20 bytes.
|
||||||
|
*/
|
||||||
|
public static final int LENGTH = 20;
|
||||||
|
|
||||||
|
/** True if P2SH, false if P2PKH. */
|
||||||
|
public final boolean p2sh;
|
||||||
|
|
||||||
|
/* Zcash P2SH header bytes */
|
||||||
|
private static int P2SH_HEADER_1 = 28;
|
||||||
|
private static int P2SH_HEADER_2 = 189;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor. Use {@link #fromBase58(NetworkParameters, String)},
|
||||||
|
* {@link #fromPubKeyHash(NetworkParameters, byte[])}, {@link #fromScriptHash(NetworkParameters, byte[])} or
|
||||||
|
* {@link #fromKey(NetworkParameters, ECKey)}.
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* network this address is valid for
|
||||||
|
* @param p2sh
|
||||||
|
* true if hash160 is hash of a script, false if it is hash of a pubkey
|
||||||
|
* @param hash160
|
||||||
|
* 20-byte hash of pubkey or script
|
||||||
|
*/
|
||||||
|
private LegacyZcashAddress(NetworkParameters params, boolean p2sh, byte[] hash160) throws AddressFormatException {
|
||||||
|
super(params, hash160);
|
||||||
|
if (hash160.length != 20)
|
||||||
|
throw new AddressFormatException.InvalidDataLength(
|
||||||
|
"Legacy addresses are 20 byte (160 bit) hashes, but got: " + hash160.length);
|
||||||
|
this.p2sh = p2sh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a {@link LegacyZcashAddress} that represents the given pubkey hash. The resulting address will be a P2PKH type of
|
||||||
|
* address.
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* network this address is valid for
|
||||||
|
* @param hash160
|
||||||
|
* 20-byte pubkey hash
|
||||||
|
* @return constructed address
|
||||||
|
*/
|
||||||
|
public static LegacyZcashAddress fromPubKeyHash(NetworkParameters params, byte[] hash160) throws AddressFormatException {
|
||||||
|
return new LegacyZcashAddress(params, false, hash160);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a {@link LegacyZcashAddress} that represents the public part of the given {@link ECKey}. Note that an address is
|
||||||
|
* derived from a hash of the public key and is not the public key itself.
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* network this address is valid for
|
||||||
|
* @param key
|
||||||
|
* only the public part is used
|
||||||
|
* @return constructed address
|
||||||
|
*/
|
||||||
|
public static LegacyZcashAddress fromKey(NetworkParameters params, ECKey key) {
|
||||||
|
return fromPubKeyHash(params, key.getPubKeyHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a {@link LegacyZcashAddress} that represents the given P2SH script hash.
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* network this address is valid for
|
||||||
|
* @param hash160
|
||||||
|
* P2SH script hash
|
||||||
|
* @return constructed address
|
||||||
|
*/
|
||||||
|
public static LegacyZcashAddress fromScriptHash(NetworkParameters params, byte[] hash160) throws AddressFormatException {
|
||||||
|
return new LegacyZcashAddress(params, true, hash160);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a {@link LegacyZcashAddress} from its base58 form.
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* expected network this address is valid for, or null if if the network should be derived from the
|
||||||
|
* base58
|
||||||
|
* @param base58
|
||||||
|
* base58-encoded textual form of the address
|
||||||
|
* @throws AddressFormatException
|
||||||
|
* if the given base58 doesn't parse or the checksum is invalid
|
||||||
|
* @throws AddressFormatException.WrongNetwork
|
||||||
|
* if the given address is valid but for a different chain (eg testnet vs mainnet)
|
||||||
|
*/
|
||||||
|
public static LegacyZcashAddress fromBase58(@Nullable NetworkParameters params, String base58)
|
||||||
|
throws AddressFormatException, AddressFormatException.WrongNetwork {
|
||||||
|
byte[] versionAndDataBytes = Base58.decodeChecked(base58);
|
||||||
|
int version = versionAndDataBytes[0] & 0xFF;
|
||||||
|
byte[] bytes = Arrays.copyOfRange(versionAndDataBytes, 1, versionAndDataBytes.length);
|
||||||
|
if (params == null) {
|
||||||
|
for (NetworkParameters p : Networks.get()) {
|
||||||
|
if (version == p.getAddressHeader())
|
||||||
|
return new LegacyZcashAddress(p, false, bytes);
|
||||||
|
else if (version == p.getP2SHHeader())
|
||||||
|
return new LegacyZcashAddress(p, true, bytes);
|
||||||
|
}
|
||||||
|
throw new AddressFormatException.InvalidPrefix("No network found for " + base58);
|
||||||
|
} else {
|
||||||
|
if (version == params.getAddressHeader())
|
||||||
|
return new LegacyZcashAddress(params, false, bytes);
|
||||||
|
else if (version == params.getP2SHHeader())
|
||||||
|
return new LegacyZcashAddress(params, true, bytes);
|
||||||
|
throw new AddressFormatException.WrongNetwork(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version header of an address. This is the first byte of a base58 encoded address.
|
||||||
|
*
|
||||||
|
* @return version header as one byte
|
||||||
|
*/
|
||||||
|
public int getVersion() {
|
||||||
|
return p2sh ? params.getP2SHHeader() : params.getAddressHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base58-encoded textual form, including version and checksum bytes.
|
||||||
|
*
|
||||||
|
* @return textual form
|
||||||
|
*/
|
||||||
|
public String toBase58() {
|
||||||
|
return this.encodeChecked(getVersion(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The (big endian) 20 byte hash that is the core of a Bitcoin address. */
|
||||||
|
@Override
|
||||||
|
public byte[] getHash() {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of output script that will be used for sending to the address. This is either
|
||||||
|
* {@link ScriptType#P2PKH} or {@link ScriptType#P2SH}.
|
||||||
|
*
|
||||||
|
* @return type of output script
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ScriptType getOutputScriptType() {
|
||||||
|
return p2sh ? ScriptType.P2SH : ScriptType.P2PKH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an address, examines the version byte and attempts to find a matching NetworkParameters. If you aren't sure
|
||||||
|
* which network the address is intended for (eg, it was provided by a user), you can use this to decide if it is
|
||||||
|
* compatible with the current wallet.
|
||||||
|
*
|
||||||
|
* @return network the address is valid for
|
||||||
|
* @throws AddressFormatException if the given base58 doesn't parse or the checksum is invalid
|
||||||
|
*/
|
||||||
|
public static NetworkParameters getParametersFromAddress(String address) throws AddressFormatException {
|
||||||
|
return LegacyZcashAddress.fromBase58(null, address).getParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o)
|
||||||
|
return true;
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
return false;
|
||||||
|
LegacyZcashAddress other = (LegacyZcashAddress) o;
|
||||||
|
return super.equals(other) && this.p2sh == other.p2sh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(super.hashCode(), p2sh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return toBase58();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LegacyZcashAddress clone() throws CloneNotSupportedException {
|
||||||
|
return (LegacyZcashAddress) super.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String encodeChecked(int version, byte[] payload) {
|
||||||
|
if (version < 0 || version > 255)
|
||||||
|
throw new IllegalArgumentException("Version not in range.");
|
||||||
|
|
||||||
|
// A stringified buffer is:
|
||||||
|
// 1 byte version + data bytes + 4 bytes check code (a truncated hash)
|
||||||
|
byte[] addressBytes = new byte[2 + payload.length + 4];
|
||||||
|
addressBytes[0] = (byte) P2SH_HEADER_1;
|
||||||
|
addressBytes[1] = (byte) P2SH_HEADER_2;
|
||||||
|
System.arraycopy(payload, 0, addressBytes, 2, payload.length);
|
||||||
|
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length + 2);
|
||||||
|
System.arraycopy(checksum, 0, addressBytes, payload.length + 2, 4);
|
||||||
|
return Base58.encode(addressBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Comparator for LegacyAddress, left argument must be LegacyAddress, right argument can be any Address
|
||||||
|
// private static final Comparator<Address> LEGACY_ADDRESS_COMPARATOR = Address.PARTIAL_ADDRESS_COMPARATOR
|
||||||
|
// .thenComparingInt(a -> ((LegacyZcashAddress) a).getVersion()) // Then compare Legacy address version byte
|
||||||
|
// .thenComparing(a -> a.bytes, UnsignedBytes.lexicographicalComparator()); // Then compare Legacy bytes
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * {@inheritDoc}
|
||||||
|
// *
|
||||||
|
// * @param o other {@code Address} object
|
||||||
|
// * @return comparison result
|
||||||
|
// */
|
||||||
|
// @Override
|
||||||
|
// public int compareTo(Address o) {
|
||||||
|
// return LEGACY_ADDRESS_COMPARATOR.compare(this, o);
|
||||||
|
// }
|
||||||
|
}
|
@ -54,7 +54,8 @@ public class Litecoin extends Bitcoiny {
|
|||||||
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
|
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||||
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
|
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||||
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
|
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
|
||||||
new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002));
|
new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002),
|
||||||
|
new Server("62.171.169.176", Server.ConnectionType.SSL, 50002));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -145,6 +146,8 @@ public class Litecoin extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(litecoinNet.getParams());
|
Context bitcoinjContext = new Context(litecoinNet.getParams());
|
||||||
|
|
||||||
instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -1,854 +0,0 @@
|
|||||||
package org.qortal.crosschain;
|
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
|
||||||
import com.google.common.primitives.Bytes;
|
|
||||||
import org.ciyam.at.*;
|
|
||||||
import org.qortal.account.Account;
|
|
||||||
import org.qortal.asset.Asset;
|
|
||||||
import org.qortal.at.QortalFunctionCode;
|
|
||||||
import org.qortal.crypto.Crypto;
|
|
||||||
import org.qortal.data.at.ATData;
|
|
||||||
import org.qortal.data.at.ATStateData;
|
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
|
||||||
import org.qortal.data.transaction.MessageTransactionData;
|
|
||||||
import org.qortal.repository.DataException;
|
|
||||||
import org.qortal.repository.Repository;
|
|
||||||
import org.qortal.utils.Base58;
|
|
||||||
import org.qortal.utils.BitTwiddling;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.ciyam.at.OpCode.calcOffset;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cross-chain trade AT
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* <ul>
|
|
||||||
* <li>Bob generates Litecoin & 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 Litecoin & Qortal 'trade' keys</li>
|
|
||||||
* <li>Alice funds Litecoin 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' Litecoin 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 Litecoin 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 Litecoin trade key and secret-A</li>
|
|
||||||
* <li>P2SH-A LTC funds end up at Litecoin address determined by redeem transaction output(s)</li>
|
|
||||||
* </ul>
|
|
||||||
* </li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
public class LitecoinACCTv2 implements ACCT {
|
|
||||||
|
|
||||||
public static final String NAME = LitecoinACCTv2.class.getSimpleName();
|
|
||||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("d5ea386a41441180c854ca8d7bbc620bfd53a97df2650a2b162b52324caf6e19").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[] partnerLitecoinPKH;
|
|
||||||
public byte[] hashOfSecretA;
|
|
||||||
public long lockTimeA;
|
|
||||||
}
|
|
||||||
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerLitecoinPKH*/ + 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 Litecoin 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 LitecoinACCTv2 instance;
|
|
||||||
|
|
||||||
private LitecoinACCTv2() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static synchronized LitecoinACCTv2 getInstance() {
|
|
||||||
if (instance == null)
|
|
||||||
instance = new LitecoinACCTv2();
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getCodeBytesHash() {
|
|
||||||
return CODE_BYTES_HASH;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getModeByteOffset() {
|
|
||||||
return MODE_BYTE_OFFSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ForeignBlockchain getBlockchain() {
|
|
||||||
return Litecoin.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 litecoinPublicKeyHash 20-byte HASH160 of creator's trade Litecoin public key
|
|
||||||
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
|
||||||
* @param litecoinAmount how much LTC the AT creator is expecting to trade
|
|
||||||
* @param tradeTimeout suggested timeout for entire trade
|
|
||||||
*/
|
|
||||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] litecoinPublicKeyHash, long qortAmount, long litecoinAmount, int tradeTimeout) {
|
|
||||||
if (litecoinPublicKeyHash.length != 20)
|
|
||||||
throw new IllegalArgumentException("Litecoin 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 addrLitecoinPublicKeyHash = addrCounter;
|
|
||||||
addrCounter += 4;
|
|
||||||
|
|
||||||
final int addrQortAmount = addrCounter++;
|
|
||||||
final int addrLitecoinAmount = 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 addrTradeMessagePartnerLitecoinPKHOffset = addrCounter++;
|
|
||||||
final int addrPartnerLitecoinPKHPointer = 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 addrPartnerLitecoinPKH = 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));
|
|
||||||
|
|
||||||
// Litecoin public key hash
|
|
||||||
assert dataByteBuffer.position() == addrLitecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrLitecoinPublicKeyHash incorrect";
|
|
||||||
dataByteBuffer.put(Bytes.ensureCapacity(litecoinPublicKeyHash, 32, 0));
|
|
||||||
|
|
||||||
// Redeem Qort amount
|
|
||||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
|
||||||
dataByteBuffer.putLong(qortAmount);
|
|
||||||
|
|
||||||
// Expected Litecoin amount
|
|
||||||
assert dataByteBuffer.position() == addrLitecoinAmount * MachineState.VALUE_SIZE : "addrLitecoinAmount incorrect";
|
|
||||||
dataByteBuffer.putLong(litecoinAmount);
|
|
||||||
|
|
||||||
// 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 Litecoin PKH
|
|
||||||
assert dataByteBuffer.position() == addrTradeMessagePartnerLitecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerLitecoinPKHOffset incorrect";
|
|
||||||
dataByteBuffer.putLong(32L);
|
|
||||||
|
|
||||||
// Index into data segment of partner's Litecoin PKH, used by GET_B_IND
|
|
||||||
assert dataByteBuffer.position() == addrPartnerLitecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerLitecoinPKHPointer incorrect";
|
|
||||||
dataByteBuffer.putLong(addrPartnerLitecoinPKH);
|
|
||||||
|
|
||||||
// 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 Litecoin 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, addrTradeMessagePartnerLitecoinPKHOffset));
|
|
||||||
// Store partner's Litecoin PKH (we only really use values from B1-B3)
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerLitecoinPKHPointer));
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
/* Sleep until message arrives */
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
|
|
||||||
|
|
||||||
// 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 LTC-QORT ACCT?", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
codeByteBuffer.flip();
|
|
||||||
|
|
||||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
|
||||||
codeByteBuffer.get(codeBytes);
|
|
||||||
|
|
||||||
assert Arrays.equals(Crypto.digest(codeBytes), LitecoinACCTv2.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.LITECOIN.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 Litecoin/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 LTC 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 Litecoin PKH
|
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
|
||||||
|
|
||||||
// Skip pointer to partner's Litecoin 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 Litecoin PKH
|
|
||||||
byte[] partnerLitecoinPKH = new byte[20];
|
|
||||||
dataByteBuffer.get(partnerLitecoinPKH);
|
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerLitecoinPKH.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 = partnerLitecoinPKH;
|
|
||||||
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.partnerLitecoinPKH = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
659
src/main/java/org/qortal/crosschain/PirateChain.java
Normal file
659
src/main/java/org/qortal/crosschain/PirateChain.java
Normal file
@ -0,0 +1,659 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.rpc.CompactFormats;
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.rust.litewalletjni.LiteWalletJni;
|
||||||
|
import org.bitcoinj.core.*;
|
||||||
|
import org.bitcoinj.crypto.ChildNumber;
|
||||||
|
import org.bitcoinj.crypto.DeterministicKey;
|
||||||
|
import org.bitcoinj.script.Script;
|
||||||
|
import org.bitcoinj.script.ScriptBuilder;
|
||||||
|
import org.bitcoinj.wallet.DeterministicKeyChain;
|
||||||
|
import org.bitcoinj.wallet.Wallet;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.libdohj.params.LitecoinRegTestParams;
|
||||||
|
import org.libdohj.params.LitecoinTestNet3Params;
|
||||||
|
import org.libdohj.params.PirateChainMainNetParams;
|
||||||
|
import org.qortal.api.model.crosschain.PirateChainSendRequest;
|
||||||
|
import org.qortal.controller.PirateChainWalletController;
|
||||||
|
import org.qortal.crosschain.PirateLightClient.Server;
|
||||||
|
import org.qortal.crosschain.PirateLightClient.Server.ConnectionType;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class PirateChain extends Bitcoiny {
|
||||||
|
|
||||||
|
public static final String CURRENCY_CODE = "ARRR";
|
||||||
|
|
||||||
|
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(10000); // 0.0001 ARRR per 1000 bytes
|
||||||
|
|
||||||
|
private static final long MINIMUM_ORDER_AMOUNT = 10000; // 0.0001 ARRR minimum order, to avoid dust errors // TODO: increase this
|
||||||
|
|
||||||
|
// Temporary values until a dynamic fee system is written.
|
||||||
|
private static final long MAINNET_FEE = 10000L; // 0.0001 ARRR
|
||||||
|
private static final long NON_MAINNET_FEE = 10000L; // 0.0001 ARRR
|
||||||
|
|
||||||
|
private static final Map<ConnectionType, Integer> DEFAULT_LITEWALLET_PORTS = new EnumMap<>(ConnectionType.class);
|
||||||
|
static {
|
||||||
|
DEFAULT_LITEWALLET_PORTS.put(ConnectionType.TCP, 9067);
|
||||||
|
DEFAULT_LITEWALLET_PORTS.put(ConnectionType.SSL, 443);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PirateChainNet {
|
||||||
|
MAIN {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return PirateChainMainNetParams.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||||
|
new Server("arrrlightd.qortal.online", ConnectionType.SSL, 443),
|
||||||
|
new Server("arrrlightd1.qortal.online", ConnectionType.SSL, 443),
|
||||||
|
new Server("arrrlightd2.qortal.online", ConnectionType.SSL, 443),
|
||||||
|
new Server("lightd.pirate.black", ConnectionType.SSL, 443));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
return "027e3758c3a65b12aa1046462b486d0a63bfa1beae327897f56c5cfb7daaae71";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
// TODO: This will need to be replaced with something better in the near future!
|
||||||
|
return MAINNET_FEE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TEST3 {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return LitecoinTestNet3Params.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
return NON_MAINNET_FEE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
REGTEST {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return LitecoinRegTestParams.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Server("localhost", ConnectionType.TCP, 9067),
|
||||||
|
new Server("localhost", ConnectionType.SSL, 443));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
// This is unique to each regtest instance
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
return NON_MAINNET_FEE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public abstract NetworkParameters getParams();
|
||||||
|
public abstract Collection<Server> getServers();
|
||||||
|
public abstract String getGenesisHash();
|
||||||
|
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PirateChain instance;
|
||||||
|
|
||||||
|
private final PirateChainNet pirateChainNet;
|
||||||
|
|
||||||
|
// Constructors and instance
|
||||||
|
|
||||||
|
private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||||
|
super(blockchain, bitcoinjContext, currencyCode);
|
||||||
|
this.pirateChainNet = pirateChainNet;
|
||||||
|
|
||||||
|
LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized PirateChain getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
PirateChainNet pirateChainNet = Settings.getInstance().getPirateChainNet();
|
||||||
|
|
||||||
|
BitcoinyBlockchainProvider pirateLightClient = new PirateLightClient("PirateChain-" + pirateChainNet.name(), pirateChainNet.getGenesisHash(), pirateChainNet.getServers(), DEFAULT_LITEWALLET_PORTS);
|
||||||
|
Context bitcoinjContext = new Context(pirateChainNet.getParams());
|
||||||
|
|
||||||
|
instance = new PirateChain(pirateChainNet, pirateLightClient, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
pirateLightClient.setBlockchain(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters & setters
|
||||||
|
|
||||||
|
public static synchronized void resetForTesting() {
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actual useful methods for use by other classes
|
||||||
|
|
||||||
|
/** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
|
||||||
|
@Override
|
||||||
|
public Coin getFeePerKb() {
|
||||||
|
return DEFAULT_FEE_PER_KB;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getMinimumOrderAmount() {
|
||||||
|
return MINIMUM_ORDER_AMOUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns estimated LTC fee, in sats per 1000bytes, optionally for historic timestamp.
|
||||||
|
*
|
||||||
|
* @param timestamp optional milliseconds since epoch, or null for 'now'
|
||||||
|
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
|
||||||
|
return this.pirateChainNet.getP2shFee(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns confirmed balance, based on passed payment script.
|
||||||
|
* <p>
|
||||||
|
* @return confirmed balance, or zero if balance unknown
|
||||||
|
* @throws ForeignBlockchainException if there was an error
|
||||||
|
*/
|
||||||
|
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
|
||||||
|
return this.blockchainProvider.getConfirmedAddressBalance(base58Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns median timestamp from latest 11 blocks, in seconds.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getMedianBlockTime() throws ForeignBlockchainException {
|
||||||
|
int height = this.blockchainProvider.getCurrentHeight();
|
||||||
|
|
||||||
|
// Grab latest 11 blocks
|
||||||
|
List<Long> blockTimestamps = this.blockchainProvider.getBlockTimestamps(height - 11, 11);
|
||||||
|
if (blockTimestamps.size() < 11)
|
||||||
|
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
|
||||||
|
|
||||||
|
// Descending order
|
||||||
|
blockTimestamps.sort((a, b) -> Long.compare(b, a));
|
||||||
|
|
||||||
|
// Pick median
|
||||||
|
return Math.toIntExact(blockTimestamps.get(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of compact blocks
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
public List<CompactFormats.CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
|
||||||
|
return this.blockchainProvider.getCompactBlocks(startHeight, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValidAddress(String address) {
|
||||||
|
// Start with some simple checks
|
||||||
|
if (address == null || !address.toLowerCase().startsWith("zs") || address.length() != 78) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try Bech32 decoding the address (which includes checksum verification)
|
||||||
|
try {
|
||||||
|
Bech32.Bech32Data decoded = Bech32.decode(address);
|
||||||
|
return (decoded != null && Objects.equals("zs", decoded.hrp));
|
||||||
|
}
|
||||||
|
catch (AddressFormatException e) {
|
||||||
|
// Invalid address, checksum failed, etc
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValidWalletKey(String walletKey) {
|
||||||
|
// For Pirate Chain, we only care that the key is a random string
|
||||||
|
// 32 characters in length, as it is used as entropy for the seed.
|
||||||
|
return walletKey != null && Base58.decode(walletKey).length == 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns 't3' prefixed P2SH address using passed redeem script. */
|
||||||
|
public String deriveP2shAddress(byte[] redeemScriptBytes) {
|
||||||
|
Context.propagate(bitcoinjContext);
|
||||||
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
return LegacyZcashAddress.fromScriptHash(this.params, redeemScriptHash).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns 'b' prefixed P2SH address using passed redeem script. */
|
||||||
|
public String deriveP2shAddressBPrefix(byte[] redeemScriptBytes) {
|
||||||
|
Context.propagate(bitcoinjContext);
|
||||||
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
return LegacyAddress.fromScriptHash(this.params, redeemScriptHash).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getWalletBalance(String entropy58) throws ForeignBlockchainException {
|
||||||
|
synchronized (this) {
|
||||||
|
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||||
|
walletController.initWithEntropy58(entropy58);
|
||||||
|
walletController.ensureInitialized();
|
||||||
|
walletController.ensureSynchronized();
|
||||||
|
walletController.ensureNotNullSeed();
|
||||||
|
|
||||||
|
// Get balance
|
||||||
|
String response = LiteWalletJni.execute("balance", "");
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
if (json.has("zbalance")) {
|
||||||
|
return json.getLong("zbalance");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForeignBlockchainException("Unable to determine balance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SimpleTransaction> getWalletTransactions(String entropy58) throws ForeignBlockchainException {
|
||||||
|
synchronized (this) {
|
||||||
|
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||||
|
walletController.initWithEntropy58(entropy58);
|
||||||
|
walletController.ensureInitialized();
|
||||||
|
walletController.ensureSynchronized();
|
||||||
|
walletController.ensureNotNullSeed();
|
||||||
|
|
||||||
|
List<SimpleTransaction> transactions = new ArrayList<>();
|
||||||
|
|
||||||
|
// Get transactions list
|
||||||
|
String response = LiteWalletJni.execute("list", "");
|
||||||
|
JSONArray transactionsJson = new JSONArray(response);
|
||||||
|
if (transactionsJson != null) {
|
||||||
|
for (int i = 0; i < transactionsJson.length(); i++) {
|
||||||
|
JSONObject transactionJson = transactionsJson.getJSONObject(i);
|
||||||
|
|
||||||
|
if (transactionJson.has("txid")) {
|
||||||
|
String txId = transactionJson.getString("txid");
|
||||||
|
Long timestamp = transactionJson.getLong("datetime");
|
||||||
|
Long amount = transactionJson.getLong("amount");
|
||||||
|
Long fee = transactionJson.getLong("fee");
|
||||||
|
String memo = null;
|
||||||
|
|
||||||
|
if (transactionJson.has("incoming_metadata")) {
|
||||||
|
JSONArray incomingMetadatas = transactionJson.getJSONArray("incoming_metadata");
|
||||||
|
if (incomingMetadatas != null) {
|
||||||
|
for (int j = 0; j < incomingMetadatas.length(); j++) {
|
||||||
|
JSONObject incomingMetadata = incomingMetadatas.getJSONObject(j);
|
||||||
|
if (incomingMetadata.has("value")) {
|
||||||
|
//String address = incomingMetadata.getString("address");
|
||||||
|
Long value = incomingMetadata.getLong("value");
|
||||||
|
amount = value; // TODO: figure out how to parse transactions with multiple incomingMetadata entries
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incomingMetadata.has("memo") && !incomingMetadata.isNull("memo")) {
|
||||||
|
memo = incomingMetadata.getString("memo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transactionJson.has("outgoing_metadata")) {
|
||||||
|
JSONArray outgoingMetadatas = transactionJson.getJSONArray("outgoing_metadata");
|
||||||
|
for (int j = 0; j < outgoingMetadatas.length(); j++) {
|
||||||
|
JSONObject outgoingMetadata = outgoingMetadatas.getJSONObject(j);
|
||||||
|
|
||||||
|
if (outgoingMetadata.has("memo") && !outgoingMetadata.isNull("memo")) {
|
||||||
|
memo = outgoingMetadata.getString("memo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long timestampMillis = Math.toIntExact(timestamp) * 1000L;
|
||||||
|
SimpleTransaction transaction = new SimpleTransaction(txId, timestampMillis, amount, fee, null, null, memo);
|
||||||
|
transactions.add(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWalletAddress(String entropy58) throws ForeignBlockchainException {
|
||||||
|
synchronized (this) {
|
||||||
|
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||||
|
walletController.initWithEntropy58(entropy58);
|
||||||
|
walletController.ensureInitialized();
|
||||||
|
walletController.ensureNotNullSeed();
|
||||||
|
|
||||||
|
return walletController.getCurrentWallet().getWalletAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUnusedReceiveAddress(String key58) throws ForeignBlockchainException {
|
||||||
|
// For now, return the main wallet address
|
||||||
|
// FUTURE: generate an unused one
|
||||||
|
return this.getWalletAddress(key58);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sendCoins(PirateChainSendRequest pirateChainSendRequest) throws ForeignBlockchainException {
|
||||||
|
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||||
|
walletController.initWithEntropy58(pirateChainSendRequest.entropy58);
|
||||||
|
walletController.ensureInitialized();
|
||||||
|
walletController.ensureSynchronized();
|
||||||
|
walletController.ensureNotNullSeed();
|
||||||
|
|
||||||
|
// Unlock wallet
|
||||||
|
walletController.getCurrentWallet().unlock();
|
||||||
|
|
||||||
|
// Build spend
|
||||||
|
JSONObject txn = new JSONObject();
|
||||||
|
txn.put("input", walletController.getCurrentWallet().getWalletAddress());
|
||||||
|
txn.put("fee", MAINNET_FEE);
|
||||||
|
|
||||||
|
JSONObject output = new JSONObject();
|
||||||
|
output.put("address", pirateChainSendRequest.receivingAddress);
|
||||||
|
output.put("amount", pirateChainSendRequest.arrrAmount);
|
||||||
|
output.put("memo", pirateChainSendRequest.memo);
|
||||||
|
|
||||||
|
JSONArray outputs = new JSONArray();
|
||||||
|
outputs.put(output);
|
||||||
|
txn.put("output", outputs);
|
||||||
|
|
||||||
|
String txnString = txn.toString();
|
||||||
|
|
||||||
|
// Send the coins
|
||||||
|
String response = LiteWalletJni.execute("send", txnString);
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
try {
|
||||||
|
if (json.has("txid")) { // Success
|
||||||
|
return json.getString("txid");
|
||||||
|
}
|
||||||
|
else if (json.has("error")) {
|
||||||
|
String error = json.getString("error");
|
||||||
|
throw new ForeignBlockchainException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new ForeignBlockchainException(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForeignBlockchainException("Something went wrong");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String fundP2SH(String entropy58, String receivingAddress, long amount,
|
||||||
|
String redeemScript58) throws ForeignBlockchainException {
|
||||||
|
|
||||||
|
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||||
|
walletController.initWithEntropy58(entropy58);
|
||||||
|
walletController.ensureInitialized();
|
||||||
|
walletController.ensureSynchronized();
|
||||||
|
walletController.ensureNotNullSeed();
|
||||||
|
|
||||||
|
// Unlock wallet
|
||||||
|
walletController.getCurrentWallet().unlock();
|
||||||
|
|
||||||
|
// Build spend
|
||||||
|
JSONObject txn = new JSONObject();
|
||||||
|
txn.put("input", walletController.getCurrentWallet().getWalletAddress());
|
||||||
|
txn.put("fee", MAINNET_FEE);
|
||||||
|
|
||||||
|
JSONObject output = new JSONObject();
|
||||||
|
output.put("address", receivingAddress);
|
||||||
|
output.put("amount", amount);
|
||||||
|
//output.put("memo", memo);
|
||||||
|
|
||||||
|
JSONArray outputs = new JSONArray();
|
||||||
|
outputs.put(output);
|
||||||
|
txn.put("output", outputs);
|
||||||
|
txn.put("script", redeemScript58);
|
||||||
|
|
||||||
|
String txnString = txn.toString();
|
||||||
|
|
||||||
|
// Send the coins
|
||||||
|
String response = LiteWalletJni.execute("sendp2sh", txnString);
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
try {
|
||||||
|
if (json.has("txid")) { // Success
|
||||||
|
return json.getString("txid");
|
||||||
|
}
|
||||||
|
else if (json.has("error")) {
|
||||||
|
String error = json.getString("error");
|
||||||
|
throw new ForeignBlockchainException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new ForeignBlockchainException(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForeignBlockchainException("Something went wrong");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String redeemP2sh(String p2shAddress, String receivingAddress, long amount, String redeemScript58,
|
||||||
|
String fundingTxid58, String secret58, String privateKey58) throws ForeignBlockchainException {
|
||||||
|
|
||||||
|
// Use null seed wallet since we may not have the entropy bytes for a real wallet's seed
|
||||||
|
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||||
|
walletController.initNullSeedWallet();
|
||||||
|
walletController.ensureInitialized();
|
||||||
|
|
||||||
|
walletController.getCurrentWallet().unlock();
|
||||||
|
|
||||||
|
// Build spend
|
||||||
|
JSONObject txn = new JSONObject();
|
||||||
|
txn.put("input", p2shAddress);
|
||||||
|
txn.put("fee", MAINNET_FEE);
|
||||||
|
|
||||||
|
JSONObject output = new JSONObject();
|
||||||
|
output.put("address", receivingAddress);
|
||||||
|
output.put("amount", amount);
|
||||||
|
// output.put("memo", ""); // Maybe useful in future to include trade details?
|
||||||
|
|
||||||
|
JSONArray outputs = new JSONArray();
|
||||||
|
outputs.put(output);
|
||||||
|
txn.put("output", outputs);
|
||||||
|
|
||||||
|
txn.put("script", redeemScript58);
|
||||||
|
txn.put("txid", fundingTxid58);
|
||||||
|
txn.put("locktime", 0); // Must be 0 when redeeming
|
||||||
|
txn.put("secret", secret58);
|
||||||
|
txn.put("privkey", privateKey58);
|
||||||
|
|
||||||
|
String txnString = txn.toString();
|
||||||
|
|
||||||
|
// Redeem the P2SH
|
||||||
|
String response = LiteWalletJni.execute("redeemp2sh", txnString);
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
try {
|
||||||
|
if (json.has("txid")) { // Success
|
||||||
|
return json.getString("txid");
|
||||||
|
}
|
||||||
|
else if (json.has("error")) {
|
||||||
|
String error = json.getString("error");
|
||||||
|
throw new ForeignBlockchainException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new ForeignBlockchainException(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForeignBlockchainException("Something went wrong");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String refundP2sh(String p2shAddress, String receivingAddress, long amount, String redeemScript58,
|
||||||
|
String fundingTxid58, int lockTime, String privateKey58) throws ForeignBlockchainException {
|
||||||
|
|
||||||
|
// Use null seed wallet since we may not have the entropy bytes for a real wallet's seed
|
||||||
|
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||||
|
walletController.initNullSeedWallet();
|
||||||
|
walletController.ensureInitialized();
|
||||||
|
|
||||||
|
walletController.getCurrentWallet().unlock();
|
||||||
|
|
||||||
|
// Build spend
|
||||||
|
JSONObject txn = new JSONObject();
|
||||||
|
txn.put("input", p2shAddress);
|
||||||
|
txn.put("fee", MAINNET_FEE);
|
||||||
|
|
||||||
|
JSONObject output = new JSONObject();
|
||||||
|
output.put("address", receivingAddress);
|
||||||
|
output.put("amount", amount);
|
||||||
|
// output.put("memo", ""); // Maybe useful in future to include trade details?
|
||||||
|
|
||||||
|
JSONArray outputs = new JSONArray();
|
||||||
|
outputs.put(output);
|
||||||
|
txn.put("output", outputs);
|
||||||
|
|
||||||
|
txn.put("script", redeemScript58);
|
||||||
|
txn.put("txid", fundingTxid58);
|
||||||
|
txn.put("locktime", lockTime);
|
||||||
|
txn.put("secret", ""); // Must be blank when refunding
|
||||||
|
txn.put("privkey", privateKey58);
|
||||||
|
|
||||||
|
String txnString = txn.toString();
|
||||||
|
|
||||||
|
// Redeem the P2SH
|
||||||
|
String response = LiteWalletJni.execute("redeemp2sh", txnString);
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
try {
|
||||||
|
if (json.has("txid")) { // Success
|
||||||
|
return json.getString("txid");
|
||||||
|
}
|
||||||
|
else if (json.has("error")) {
|
||||||
|
String error = json.getString("error");
|
||||||
|
throw new ForeignBlockchainException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new ForeignBlockchainException(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForeignBlockchainException("Something went wrong");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSyncStatus(String entropy58) throws ForeignBlockchainException {
|
||||||
|
synchronized (this) {
|
||||||
|
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
|
||||||
|
walletController.initWithEntropy58(entropy58);
|
||||||
|
|
||||||
|
return walletController.getSyncStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BitcoinyTransaction deserializeRawTransaction(String rawTransactionHex) throws TransformationException {
|
||||||
|
byte[] rawTransactionData = HashCode.fromString(rawTransactionHex).asBytes();
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.wrap(rawTransactionData);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
int header = BitTwiddling.readU32(byteBuffer);
|
||||||
|
boolean overwintered = ((header >> 31 & 0xff) == 255);
|
||||||
|
int version = header & 0x7FFFFFFF;
|
||||||
|
|
||||||
|
// Version group ID
|
||||||
|
int versionGroupId = 0;
|
||||||
|
if (overwintered) {
|
||||||
|
versionGroupId = BitTwiddling.readU32(byteBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isOverwinterV3 = overwintered && versionGroupId == 0x03C48270 && version == 3;
|
||||||
|
boolean isSaplingV4 = overwintered && versionGroupId == 0x892F2085 && version == 4;
|
||||||
|
if (overwintered && !(isOverwinterV3 || isSaplingV4)) {
|
||||||
|
throw new TransformationException("Unknown transaction format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inputs
|
||||||
|
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
|
||||||
|
int vinCount = BitTwiddling.readU8(byteBuffer);
|
||||||
|
for (int i=0; i<vinCount; i++) {
|
||||||
|
// Outpoint hash
|
||||||
|
byte[] outpointHashBytes = new byte[32];
|
||||||
|
byteBuffer.get(outpointHashBytes);
|
||||||
|
String outpointHash = HashCode.fromBytes(outpointHashBytes).toString();
|
||||||
|
|
||||||
|
// vout
|
||||||
|
int vout = BitTwiddling.readU32(byteBuffer);
|
||||||
|
|
||||||
|
// scriptSig
|
||||||
|
int scriptSigLength = BitTwiddling.readU8(byteBuffer);
|
||||||
|
byte[] scriptSigBytes = new byte[scriptSigLength];
|
||||||
|
byteBuffer.get(scriptSigBytes);
|
||||||
|
String scriptSig = HashCode.fromBytes(scriptSigBytes).toString();
|
||||||
|
|
||||||
|
int sequence = BitTwiddling.readU32(byteBuffer);
|
||||||
|
|
||||||
|
BitcoinyTransaction.Input input = new BitcoinyTransaction.Input(scriptSig, sequence, outpointHash, vout);
|
||||||
|
inputs.add(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
|
||||||
|
int voutCount = BitTwiddling.readU8(byteBuffer);
|
||||||
|
for (int i=0; i<voutCount; i++) {
|
||||||
|
// Amount
|
||||||
|
byte[] amountBytes = new byte[8];
|
||||||
|
byteBuffer.get(amountBytes);
|
||||||
|
long amount = BitTwiddling.longFromLEBytes(amountBytes, 0);
|
||||||
|
|
||||||
|
// Script pubkey
|
||||||
|
int scriptPubkeySize = BitTwiddling.readU8(byteBuffer);
|
||||||
|
byte[] scriptPubkeyBytes = new byte[scriptPubkeySize];
|
||||||
|
byteBuffer.get(scriptPubkeyBytes);
|
||||||
|
String scriptPubKey = HashCode.fromBytes(scriptPubkeyBytes).toString();
|
||||||
|
|
||||||
|
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, amount, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locktime
|
||||||
|
byte[] locktimeBytes = new byte[4];
|
||||||
|
byteBuffer.get(locktimeBytes);
|
||||||
|
int locktime = BitTwiddling.intFromLEBytes(locktimeBytes, 0);
|
||||||
|
|
||||||
|
// Expiry height
|
||||||
|
int expiryHeight = 0;
|
||||||
|
if (isOverwinterV3 || isSaplingV4) {
|
||||||
|
byte[] expiryHeightBytes = new byte[4];
|
||||||
|
byteBuffer.get(expiryHeightBytes);
|
||||||
|
expiryHeight = BitTwiddling.intFromLEBytes(expiryHeightBytes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
String txHash = null; // Not present in raw transaction data
|
||||||
|
int size = 0; // Not present in raw transaction data
|
||||||
|
Integer timestamp = null; // Not present in raw transaction data
|
||||||
|
|
||||||
|
// Note: this is incomplete, as sapling spend info is not yet parsed. We don't need it for our
|
||||||
|
// current trade bot implementation, but it could be added in the future, for completeness.
|
||||||
|
// See link below for reference:
|
||||||
|
// https://github.com/PirateNetwork/librustzcash/blob/2981c4d2860f7cd73282fed885daac0323ff0280/zcash_primitives/src/transaction/mod.rs#L197
|
||||||
|
|
||||||
|
return new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,8 +2,6 @@ package org.qortal.crosschain;
|
|||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.ciyam.at.*;
|
import org.ciyam.at.*;
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
@ -29,7 +27,7 @@ import static org.ciyam.at.OpCode.calcOffset;
|
|||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Bob generates Dogecoin & Qortal 'trade' keys
|
* <li>Bob generates PirateChain & Qortal 'trade' keys
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>private key required to sign P2SH redeem tx</li>
|
* <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>private key could be used to create 'secret' (e.g. double-SHA256)</li>
|
||||||
@ -42,12 +40,12 @@ import static org.ciyam.at.OpCode.calcOffset;
|
|||||||
* </li>
|
* </li>
|
||||||
* <li>Alice finds Qortal AT and wants to trade
|
* <li>Alice finds Qortal AT and wants to trade
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Alice generates Dogecoin & Qortal 'trade' keys</li>
|
* <li>Alice generates PirateChain & Qortal 'trade' keys</li>
|
||||||
* <li>Alice funds Dogecoin P2SH-A</li>
|
* <li>Alice funds PirateChain P2SH-A</li>
|
||||||
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
|
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>hash-of-secret-A</li>
|
* <li>hash-of-secret-A</li>
|
||||||
* <li>her 'trade' Dogecoin PKH</li>
|
* <li>her 'trade' Pirate Chain public key</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* </li>
|
* </li>
|
||||||
* </ul>
|
* </ul>
|
||||||
@ -58,7 +56,7 @@ import static org.ciyam.at.OpCode.calcOffset;
|
|||||||
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
|
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Alice's trade Qortal address</li>
|
* <li>Alice's trade Qortal address</li>
|
||||||
* <li>Alice's trade Dogecoin PKH</li>
|
* <li>Alice's trade Pirate Chain public key</li>
|
||||||
* <li>hash-of-secret-A</li>
|
* <li>hash-of-secret-A</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* </li>
|
* </li>
|
||||||
@ -77,48 +75,46 @@ import static org.ciyam.at.OpCode.calcOffset;
|
|||||||
* </li>
|
* </li>
|
||||||
* <li>Bob checks AT, extracts secret-A
|
* <li>Bob checks AT, extracts secret-A
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Bob redeems P2SH-A using his Dogecoin trade key and secret-A</li>
|
* <li>Bob redeems P2SH-A using his PirateChain trade key and secret-A</li>
|
||||||
* <li>P2SH-A DOGE funds end up at Dogecoin address determined by redeem transaction output(s)</li>
|
* <li>P2SH-A ARRR funds end up at PirateChain address determined by redeem transaction output(s)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* </li>
|
* </li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public class DogecoinACCTv2 implements ACCT {
|
public class PirateChainACCTv3 implements ACCT {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2.class);
|
public static final String NAME = PirateChainACCTv3.class.getSimpleName();
|
||||||
|
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("fc2818ac0819ab658a065ab0d050e75f167921e2dce5969b9b7741e47e477d83").asBytes(); // SHA256 of AT code bytes
|
||||||
public static final String NAME = DogecoinACCTv2.class.getSimpleName();
|
|
||||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("6fff38d6eeb06568a9c879c5628527730319844aa0de53f5f4ffab5506efe885").asBytes(); // SHA256 of AT code bytes
|
|
||||||
|
|
||||||
public static final int SECRET_LENGTH = 32;
|
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). */
|
/** <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;
|
private static final int MODE_VALUE_OFFSET = 68;
|
||||||
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */
|
/** <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 final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
|
||||||
|
|
||||||
public static class OfferMessageData {
|
public static class OfferMessageData {
|
||||||
public byte[] partnerDogecoinPKH;
|
public byte[] partnerPirateChainPublicKey;
|
||||||
public byte[] hashOfSecretA;
|
public byte[] hashOfSecretA;
|
||||||
public long lockTimeA;
|
public long lockTimeA;
|
||||||
}
|
}
|
||||||
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDogecoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
|
public static final int OFFER_MESSAGE_LENGTH = 33 /*partnerPirateChainPublicKey*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
|
||||||
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
|
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
|
||||||
+ 24 /*partner's Dogecoin PKH (padded from 20 to 24)*/
|
+ 40 /*partner's Pirate Chain public key (padded from 33 to 40)*/
|
||||||
+ 8 /*AT trade timeout (minutes)*/
|
+ 8 /*AT trade timeout (minutes)*/
|
||||||
+ 24 /*hash of secret-A (padded from 20 to 24)*/
|
+ 24 /*hash of secret-A (padded from 20 to 24)*/
|
||||||
+ 8 /*lockTimeA*/;
|
+ 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 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*/;
|
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
|
||||||
|
|
||||||
private static DogecoinACCTv2 instance;
|
private static PirateChainACCTv3 instance;
|
||||||
|
|
||||||
private DogecoinACCTv2() {
|
private PirateChainACCTv3() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized DogecoinACCTv2 getInstance() {
|
public static synchronized PirateChainACCTv3 getInstance() {
|
||||||
if (instance == null)
|
if (instance == null)
|
||||||
instance = new DogecoinACCTv2();
|
instance = new PirateChainACCTv3();
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@ -135,7 +131,7 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ForeignBlockchain getBlockchain() {
|
public ForeignBlockchain getBlockchain() {
|
||||||
return Dogecoin.getInstance();
|
return PirateChain.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,14 +141,14 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
|
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
|
||||||
*
|
*
|
||||||
* @param creatorTradeAddress AT creator's trade Qortal address
|
* @param creatorTradeAddress AT creator's trade Qortal address
|
||||||
* @param dogecoinPublicKeyHash 20-byte HASH160 of creator's trade Dogecoin public key
|
* @param pirateChainPublicKeyHash 33-byte creator's trade PirateChain public key
|
||||||
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
|
||||||
* @param dogecoinAmount how much DOGE the AT creator is expecting to trade
|
* @param arrrAmount how much ARRR the AT creator is expecting to trade
|
||||||
* @param tradeTimeout suggested timeout for entire trade
|
* @param tradeTimeout suggested timeout for entire trade
|
||||||
*/
|
*/
|
||||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] dogecoinPublicKeyHash, long qortAmount, long dogecoinAmount, int tradeTimeout) {
|
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] pirateChainPublicKeyHash, long qortAmount, long arrrAmount, int tradeTimeout) {
|
||||||
if (dogecoinPublicKeyHash.length != 20)
|
if (pirateChainPublicKeyHash.length != 33)
|
||||||
throw new IllegalArgumentException("Dogecoin public key hash should be 20 bytes");
|
throw new IllegalArgumentException("PirateChain public key hash should be 33 bytes");
|
||||||
|
|
||||||
// Labels for data segment addresses
|
// Labels for data segment addresses
|
||||||
int addrCounter = 0;
|
int addrCounter = 0;
|
||||||
@ -164,11 +160,11 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
final int addrCreatorTradeAddress3 = addrCounter++;
|
final int addrCreatorTradeAddress3 = addrCounter++;
|
||||||
final int addrCreatorTradeAddress4 = addrCounter++;
|
final int addrCreatorTradeAddress4 = addrCounter++;
|
||||||
|
|
||||||
final int addrDogecoinPublicKeyHash = addrCounter;
|
final int addrPirateChainPublicKeyHash = addrCounter;
|
||||||
addrCounter += 4;
|
addrCounter += 5;
|
||||||
|
|
||||||
final int addrQortAmount = addrCounter++;
|
final int addrQortAmount = addrCounter++;
|
||||||
final int addrDogecoinAmount = addrCounter++;
|
final int addrarrrAmount = addrCounter++;
|
||||||
final int addrTradeTimeout = addrCounter++;
|
final int addrTradeTimeout = addrCounter++;
|
||||||
|
|
||||||
final int addrMessageTxnType = addrCounter++;
|
final int addrMessageTxnType = addrCounter++;
|
||||||
@ -179,8 +175,10 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
final int addrQortalPartnerAddressPointer = addrCounter++;
|
final int addrQortalPartnerAddressPointer = addrCounter++;
|
||||||
final int addrMessageSenderPointer = addrCounter++;
|
final int addrMessageSenderPointer = addrCounter++;
|
||||||
|
|
||||||
final int addrTradeMessagePartnerDogecoinPKHOffset = addrCounter++;
|
final int addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset = addrCounter++;
|
||||||
final int addrPartnerDogecoinPKHPointer = addrCounter++;
|
final int addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset = addrCounter++; // Remainder of public key, plus timeout
|
||||||
|
final int addrPartnerPirateChainPublicKeyFirst32BytesPointer = addrCounter++;
|
||||||
|
final int addrPartnerPirateChainPublicKeyLastBytePointer = addrCounter++; // Remainder of public key
|
||||||
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
|
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
|
||||||
final int addrHashOfSecretAPointer = addrCounter++;
|
final int addrHashOfSecretAPointer = addrCounter++;
|
||||||
|
|
||||||
@ -226,9 +224,12 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
final int addrHashOfSecretA = addrCounter;
|
final int addrHashOfSecretA = addrCounter;
|
||||||
addrCounter += 4;
|
addrCounter += 4;
|
||||||
|
|
||||||
final int addrPartnerDogecoinPKH = addrCounter;
|
final int addrPartnerPirateChainPublicKeyFirst32Bytes = addrCounter;
|
||||||
addrCounter += 4;
|
addrCounter += 4;
|
||||||
|
|
||||||
|
final int addrPartnerPirateChainPublicKeyLastByte = addrCounter;
|
||||||
|
addrCounter += 4; // We retrieve using GET_B_IND, so need to allow space for the full 32 bytes
|
||||||
|
|
||||||
final int addrPartnerReceivingAddress = addrCounter;
|
final int addrPartnerReceivingAddress = addrCounter;
|
||||||
addrCounter += 4;
|
addrCounter += 4;
|
||||||
|
|
||||||
@ -243,17 +244,17 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
|
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
|
||||||
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
|
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
|
||||||
|
|
||||||
// Dogecoin public key hash
|
// PirateChain public key hash
|
||||||
assert dataByteBuffer.position() == addrDogecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrDogecoinPublicKeyHash incorrect";
|
assert dataByteBuffer.position() == addrPirateChainPublicKeyHash * MachineState.VALUE_SIZE : "addrPirateChainPublicKeyHash incorrect";
|
||||||
dataByteBuffer.put(Bytes.ensureCapacity(dogecoinPublicKeyHash, 32, 0));
|
dataByteBuffer.put(Bytes.ensureCapacity(pirateChainPublicKeyHash, 40, 0));
|
||||||
|
|
||||||
// Redeem Qort amount
|
// Redeem Qort amount
|
||||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||||
dataByteBuffer.putLong(qortAmount);
|
dataByteBuffer.putLong(qortAmount);
|
||||||
|
|
||||||
// Expected Dogecoin amount
|
// Expected PirateChain amount
|
||||||
assert dataByteBuffer.position() == addrDogecoinAmount * MachineState.VALUE_SIZE : "addrDogecoinAmount incorrect";
|
assert dataByteBuffer.position() == addrarrrAmount * MachineState.VALUE_SIZE : "addrarrrAmount incorrect";
|
||||||
dataByteBuffer.putLong(dogecoinAmount);
|
dataByteBuffer.putLong(arrrAmount);
|
||||||
|
|
||||||
// Suggested trade timeout (minutes)
|
// Suggested trade timeout (minutes)
|
||||||
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
|
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
|
||||||
@ -283,17 +284,25 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
|
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
|
||||||
dataByteBuffer.putLong(addrMessageSender1);
|
dataByteBuffer.putLong(addrMessageSender1);
|
||||||
|
|
||||||
// Offset into 'trade' MESSAGE data payload for extracting partner's Dogecoin PKH
|
// Offset into 'trade' MESSAGE data payload for extracting first 32 bytes of partner's Pirate Chain public key
|
||||||
assert dataByteBuffer.position() == addrTradeMessagePartnerDogecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDogecoinPKHOffset incorrect";
|
assert dataByteBuffer.position() == addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset incorrect";
|
||||||
dataByteBuffer.putLong(32L);
|
dataByteBuffer.putLong(32L);
|
||||||
|
|
||||||
// Index into data segment of partner's Dogecoin PKH, used by GET_B_IND
|
// Offset into 'trade' MESSAGE data payload for extracting last byte of public key
|
||||||
assert dataByteBuffer.position() == addrPartnerDogecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerDogecoinPKHPointer incorrect";
|
assert dataByteBuffer.position() == addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset incorrect";
|
||||||
dataByteBuffer.putLong(addrPartnerDogecoinPKH);
|
dataByteBuffer.putLong(64L);
|
||||||
|
|
||||||
|
// Index into data segment of partner's Pirate Chain public key, used by GET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrPartnerPirateChainPublicKeyFirst32BytesPointer * MachineState.VALUE_SIZE : "addrPartnerPirateChainPublicKeyFirst32BytesPointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrPartnerPirateChainPublicKeyFirst32Bytes);
|
||||||
|
|
||||||
|
// Index into data segment of remainder of partner's Pirate Chain public key, used by GET_B_IND
|
||||||
|
assert dataByteBuffer.position() == addrPartnerPirateChainPublicKeyLastBytePointer * MachineState.VALUE_SIZE : "addrPartnerPirateChainPublicKeyLastBytePointer incorrect";
|
||||||
|
dataByteBuffer.putLong(addrPartnerPirateChainPublicKeyLastByte);
|
||||||
|
|
||||||
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
|
||||||
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
|
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
|
||||||
dataByteBuffer.putLong(64L);
|
dataByteBuffer.putLong(80L);
|
||||||
|
|
||||||
// Index into data segment to hash of secret A, used by GET_B_IND
|
// Index into data segment to hash of secret A, used by GET_B_IND
|
||||||
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
|
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
|
||||||
@ -338,9 +347,6 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
try {
|
try {
|
||||||
/* Initialization */
|
/* Initialization */
|
||||||
|
|
||||||
/* NOP - to ensure DOGECOIN ACCT is unique */
|
|
||||||
codeByteBuffer.put(OpCode.NOP.compile());
|
|
||||||
|
|
||||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
// 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));
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
|
||||||
|
|
||||||
@ -429,12 +435,17 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
|
// 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));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
|
||||||
|
|
||||||
// Extract trade partner's Dogecoin public key hash (PKH) from message into B
|
// Extract first 32 bytes of trade partner's Pirate Chain public key from message into B
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDogecoinPKHOffset));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset));
|
||||||
// Store partner's Dogecoin PKH (we only really use values from B1-B3)
|
// Store first 32 bytes of partner's Pirate Chain public key
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDogecoinPKHPointer));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerPirateChainPublicKeyFirst32BytesPointer));
|
||||||
// Extract AT trade timeout (minutes) (from B4)
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
|
// Extract last byte of public key, plus trade timeout, from message into B
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset));
|
||||||
|
// Store last byte of partner's Pirate Chain public key
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerPirateChainPublicKeyLastBytePointer));
|
||||||
|
// Extract AT trade timeout (minutes) (from B2)
|
||||||
|
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B2, addrRefundTimeout));
|
||||||
|
|
||||||
// Grab next 32 bytes
|
// Grab next 32 bytes
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
|
||||||
@ -465,9 +476,6 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
/* Transaction processing loop */
|
/* Transaction processing loop */
|
||||||
labelRedeemTxnLoop = codeByteBuffer.position();
|
labelRedeemTxnLoop = codeByteBuffer.position();
|
||||||
|
|
||||||
/* Sleep until message arrives */
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
|
|
||||||
|
|
||||||
// Find next transaction to this AT since the last one (if any)
|
// 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));
|
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.
|
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
|
||||||
@ -546,7 +554,7 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
|
||||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||||
} catch (CompilationException e) {
|
} catch (CompilationException e) {
|
||||||
throw new IllegalStateException("Unable to compile DOGE-QORT ACCT?", e);
|
throw new IllegalStateException("Unable to compile ARRR-QORT ACCT?", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,7 +563,7 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||||
codeByteBuffer.get(codeBytes);
|
codeByteBuffer.get(codeBytes);
|
||||||
|
|
||||||
assert Arrays.equals(Crypto.digest(codeBytes), DogecoinACCTv2.CODE_BYTES_HASH)
|
assert Arrays.equals(Crypto.digest(codeBytes), PirateChainACCTv3.CODE_BYTES_HASH)
|
||||||
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
|
: 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 ciyamAtVersion = 2;
|
||||||
@ -593,7 +601,7 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
|
|
||||||
CrossChainTradeData tradeData = new CrossChainTradeData();
|
CrossChainTradeData tradeData = new CrossChainTradeData();
|
||||||
|
|
||||||
tradeData.foreignBlockchain = SupportedBlockchain.DOGECOIN.name();
|
tradeData.foreignBlockchain = SupportedBlockchain.PIRATECHAIN.name();
|
||||||
tradeData.acctName = NAME;
|
tradeData.acctName = NAME;
|
||||||
|
|
||||||
tradeData.qortalAtAddress = atAddress;
|
tradeData.qortalAtAddress = atAddress;
|
||||||
@ -614,10 +622,10 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
|
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
|
||||||
|
|
||||||
// Creator's Dogecoin/foreign public key hash
|
// Creator's PirateChain/foreign public key (full 33 bytes, not hashed, so ignore references to "PKH")
|
||||||
tradeData.creatorForeignPKH = new byte[20];
|
tradeData.creatorForeignPKH = new byte[33];
|
||||||
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
dataByteBuffer.get(tradeData.creatorForeignPKH);
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
|
dataByteBuffer.position(dataByteBuffer.position() + 40 - tradeData.creatorForeignPKH.length); // skip to 40 bytes
|
||||||
|
|
||||||
// We don't use secret-B
|
// We don't use secret-B
|
||||||
tradeData.hashOfSecretB = null;
|
tradeData.hashOfSecretB = null;
|
||||||
@ -625,7 +633,7 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
// Redeem payout
|
// Redeem payout
|
||||||
tradeData.qortAmount = dataByteBuffer.getLong();
|
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
// Expected DOGE amount
|
// Expected ARRR amount
|
||||||
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
|
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
// Trade timeout
|
// Trade timeout
|
||||||
@ -649,10 +657,16 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
// Skip pointer to message sender
|
// Skip pointer to message sender
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
// Skip 'trade' message data offset for partner's Dogecoin PKH
|
// Skip 'trade' message data offset for first 32 bytes of partner's Pirate Chain public key
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
// Skip pointer to partner's Dogecoin PKH
|
// Skip 'trade' message data offset for last 32 byte of partner's Pirate Chain public key
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to partner's Pirate Chain public key (first 32 bytes)
|
||||||
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
|
// Skip pointer to partner's Pirate Chain public key (last byte)
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||||
|
|
||||||
// Skip 'trade' message data offset for hash-of-secret-A
|
// Skip 'trade' message data offset for hash-of-secret-A
|
||||||
@ -718,10 +732,10 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
dataByteBuffer.get(hashOfSecretA);
|
dataByteBuffer.get(hashOfSecretA);
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
|
||||||
|
|
||||||
// Potential partner's Dogecoin PKH
|
// Potential partner's PirateChain public key
|
||||||
byte[] partnerDogecoinPKH = new byte[20];
|
byte[] partnerPirateChainPublicKey = new byte[33];
|
||||||
dataByteBuffer.get(partnerDogecoinPKH);
|
dataByteBuffer.get(partnerPirateChainPublicKey);
|
||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDogecoinPKH.length); // skip to 32 bytes
|
dataByteBuffer.position(dataByteBuffer.position() + 64 - partnerPirateChainPublicKey.length); // skip to 64 bytes
|
||||||
|
|
||||||
// Partner's receiving address (if present)
|
// Partner's receiving address (if present)
|
||||||
byte[] partnerReceivingAddress = new byte[25];
|
byte[] partnerReceivingAddress = new byte[25];
|
||||||
@ -740,7 +754,7 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
|
||||||
tradeData.qortalPartnerAddress = qortalRecipient;
|
tradeData.qortalPartnerAddress = qortalRecipient;
|
||||||
tradeData.hashOfSecretA = hashOfSecretA;
|
tradeData.hashOfSecretA = hashOfSecretA;
|
||||||
tradeData.partnerForeignPKH = partnerDogecoinPKH;
|
tradeData.partnerForeignPKH = partnerPirateChainPublicKey; // Not hashed
|
||||||
tradeData.lockTimeA = lockTimeA;
|
tradeData.lockTimeA = lockTimeA;
|
||||||
|
|
||||||
if (mode == AcctMode.REDEEMED)
|
if (mode == AcctMode.REDEEMED)
|
||||||
@ -755,9 +769,9 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
|
/** 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) {
|
public static byte[] buildOfferMessage(byte[] partnerBitcoinPublicKey, byte[] hashOfSecretA, int lockTimeA) {
|
||||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||||
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
|
return Bytes.concat(partnerBitcoinPublicKey, hashOfSecretA, lockTimeABytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
|
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
|
||||||
@ -766,25 +780,25 @@ public class DogecoinACCTv2 implements ACCT {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
OfferMessageData offerMessageData = new OfferMessageData();
|
OfferMessageData offerMessageData = new OfferMessageData();
|
||||||
offerMessageData.partnerDogecoinPKH = Arrays.copyOfRange(messageData, 0, 20);
|
offerMessageData.partnerPirateChainPublicKey = Arrays.copyOfRange(messageData, 0, 33);
|
||||||
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
|
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 33, 53);
|
||||||
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
|
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 53);
|
||||||
|
|
||||||
return offerMessageData;
|
return offerMessageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
|
/** 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) {
|
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPublicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
|
||||||
byte[] data = new byte[TRADE_MESSAGE_LENGTH];
|
byte[] data = new byte[TRADE_MESSAGE_LENGTH];
|
||||||
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
|
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
|
||||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||||
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
|
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
|
||||||
|
|
||||||
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
|
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
|
||||||
System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
|
System.arraycopy(partnerBitcoinPublicKey, 0, data, 32, partnerBitcoinPublicKey.length);
|
||||||
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
|
System.arraycopy(refundTimeoutBytes, 0, data, 72, refundTimeoutBytes.length);
|
||||||
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
|
System.arraycopy(hashOfSecretA, 0, data, 80, hashOfSecretA.length);
|
||||||
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
|
System.arraycopy(lockTimeABytes, 0, data, 104, lockTimeABytes.length);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
404
src/main/java/org/qortal/crosschain/PirateChainHTLC.java
Normal file
404
src/main/java/org/qortal/crosschain/PirateChainHTLC.java
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
import org.bitcoinj.core.*;
|
||||||
|
import org.bitcoinj.script.Script;
|
||||||
|
import org.bitcoinj.script.ScriptChunk;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import static org.qortal.crosschain.BitcoinyHTLC.Status;
|
||||||
|
|
||||||
|
public class PirateChainHTLC {
|
||||||
|
|
||||||
|
public static final int SECRET_LENGTH = 32;
|
||||||
|
public static final int MIN_LOCKTIME = 1500000000;
|
||||||
|
|
||||||
|
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
|
||||||
|
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
|
||||||
|
|
||||||
|
// Assuming node's trade-bot has no more than 100 entries?
|
||||||
|
private static final int MAX_CACHE_ENTRIES = 100;
|
||||||
|
|
||||||
|
// Max time-to-live for cache entries (milliseconds)
|
||||||
|
private static final long CACHE_TIMEOUT = 30_000L;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
private static final Map<String, byte[]> SECRET_CACHE = new LinkedHashMap<>(MAX_CACHE_ENTRIES + 1, 0.75F, true) {
|
||||||
|
// This method is called just after a new entry has been added
|
||||||
|
@Override
|
||||||
|
public boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
|
||||||
|
return size() > MAX_CACHE_ENTRIES;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private static final byte[] NO_SECRET_CACHE_ENTRY = new byte[0];
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
private static final Map<String, Status> STATUS_CACHE = new LinkedHashMap<>(MAX_CACHE_ENTRIES + 1, 0.75F, true) {
|
||||||
|
// This method is called just after a new entry has been added
|
||||||
|
@Override
|
||||||
|
public boolean removeEldestEntry(Map.Entry<String, Status> eldest) {
|
||||||
|
return size() > MAX_CACHE_ENTRIES;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* OP_RETURN + OP_PUSHDATA1 + bytes (not part of actual redeem script - used for "push only" secondary output when funding P2SH)
|
||||||
|
*
|
||||||
|
* OP_IF (if top stack value isn't false) (true=refund; false=redeem) (boolean is then removed from stack)
|
||||||
|
* <push 4 bytes> <intended locktime>
|
||||||
|
* OP_CHECKLOCKTIMEVERIFY (if stack locktime greater than transaction's lock time - i.e. refunding but too soon - then fail validation)
|
||||||
|
* OP_DROP (remove locktime from top of stack)
|
||||||
|
* <push 33 bytes> <intended refunder public key>
|
||||||
|
* OP_CHECKSIG (check signature and public key are correct; returns 1 or 0)
|
||||||
|
* OP_ELSE (if top stack value was false, i.e. attempting to redeem)
|
||||||
|
* OP_SIZE (push length of top item - the secret - to the top of the stack)
|
||||||
|
* <push 1 byte> 32
|
||||||
|
* OP_EQUALVERIFY (unhashed secret must be 32 bytes in length)
|
||||||
|
* OP_HASH160 (hash the secret)
|
||||||
|
* <push 20 bytes> <intended secret hash>
|
||||||
|
* OP_EQUALVERIFY (ensure hash of supplied secret matches intended secret hash; transaction invalid if no match)
|
||||||
|
* <push 33 bytes> <intended redeemer public key>
|
||||||
|
* OP_CHECKSIG (check signature and public key are correct; returns 1 or 0)
|
||||||
|
* OP_ENDIF
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static final byte[] pushOnlyPrefix = HashCode.fromString("6a4c").asBytes(); // OP_RETURN + push(redeem script)
|
||||||
|
private static final byte[] redeemScript1 = HashCode.fromString("6304").asBytes(); // OP_IF push(4 bytes locktime)
|
||||||
|
private static final byte[] redeemScript2 = HashCode.fromString("b17521").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_DROP push(33 bytes refund pubkey)
|
||||||
|
private static final byte[] redeemScript3 = HashCode.fromString("ac6782012088a914").asBytes(); // OP_CHECKSIG OP_ELSE OP_SIZE push(0x20) OP_EQUALVERIFY OP_HASH160 push(20 bytes hash of secret)
|
||||||
|
private static final byte[] redeemScript4 = HashCode.fromString("8821").asBytes(); // OP_EQUALVERIFY push(33 bytes redeem pubkey)
|
||||||
|
private static final byte[] redeemScript5 = HashCode.fromString("ac68").asBytes(); // OP_CHECKSIG OP_ENDIF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns redeemScript used for cross-chain trading.
|
||||||
|
* <p>
|
||||||
|
* See comments in {@link PirateChainHTLC} for more details.
|
||||||
|
*
|
||||||
|
* @param refunderPubKey 33-byte P2SH funder's public key, for refunding purposes
|
||||||
|
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||||
|
* @param redeemerPubKey 33-byte P2SH redeemer's public key
|
||||||
|
* @param hashOfSecret 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||||
|
*/
|
||||||
|
public static byte[] buildScript(byte[] refunderPubKey, int lockTime, byte[] redeemerPubKey, byte[] hashOfSecret) {
|
||||||
|
return Bytes.concat(redeemScript1, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), redeemScript2,
|
||||||
|
refunderPubKey, redeemScript3, hashOfSecret, redeemScript4, redeemerPubKey, redeemScript5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative to buildScript() above, this time with a prefix suitable for adding the redeem script
|
||||||
|
* to a "push only" output (via OP_RETURN followed by OP_PUSHDATA1)
|
||||||
|
*
|
||||||
|
* @param refunderPubKey 33-byte P2SH funder's public key, for refunding purposes
|
||||||
|
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||||
|
* @param redeemerPubKey 33-byte P2SH redeemer's public key
|
||||||
|
* @param hashOfSecret 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static byte[] buildScriptWithPrefix(byte[] refunderPubKey, int lockTime, byte[] redeemerPubKey, byte[] hashOfSecret) {
|
||||||
|
byte[] redeemScript = buildScript(refunderPubKey, lockTime, redeemerPubKey, hashOfSecret);
|
||||||
|
int size = redeemScript.length;
|
||||||
|
String sizeHex = Integer.toHexString(size & 0xFF);
|
||||||
|
return Bytes.concat(pushOnlyPrefix, HashCode.fromString(sizeHex).asBytes(), redeemScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns 'secret', if any, given HTLC's P2SH address.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException
|
||||||
|
*/
|
||||||
|
public static byte[] findHtlcSecret(Bitcoiny bitcoiny, String p2shAddress) throws ForeignBlockchainException {
|
||||||
|
NetworkParameters params = bitcoiny.getNetworkParameters();
|
||||||
|
String compoundKey = String.format("%s-%s-%d", params.getId(), p2shAddress, System.currentTimeMillis() / CACHE_TIMEOUT);
|
||||||
|
|
||||||
|
byte[] secret = SECRET_CACHE.getOrDefault(compoundKey, NO_SECRET_CACHE_ENTRY);
|
||||||
|
if (secret != NO_SECRET_CACHE_ENTRY)
|
||||||
|
return secret;
|
||||||
|
|
||||||
|
List<byte[]> rawTransactions = bitcoiny.getAddressTransactions(p2shAddress);
|
||||||
|
|
||||||
|
for (byte[] rawTransaction : rawTransactions) {
|
||||||
|
Transaction transaction = new Transaction(params, rawTransaction);
|
||||||
|
|
||||||
|
// Cycle through inputs, looking for one that spends our HTLC
|
||||||
|
for (TransactionInput input : transaction.getInputs()) {
|
||||||
|
Script scriptSig = input.getScriptSig();
|
||||||
|
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
|
||||||
|
|
||||||
|
// Expected number of script chunks for redeem. Refund might not have the same number.
|
||||||
|
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
|
||||||
|
if (scriptChunks.size() != expectedChunkCount)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// We're expecting last chunk to contain the actual redeemScript
|
||||||
|
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
|
||||||
|
byte[] redeemScriptBytes = lastChunk.data;
|
||||||
|
|
||||||
|
// If non-push scripts, redeemScript will be null
|
||||||
|
if (redeemScriptBytes == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
|
||||||
|
if (!inputAddress.toString().equals(p2shAddress))
|
||||||
|
// Input isn't spending our HTLC
|
||||||
|
continue;
|
||||||
|
|
||||||
|
secret = scriptChunks.get(0).data;
|
||||||
|
if (secret.length != PirateChainHTLC.SECRET_LENGTH)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Cache secret for a while
|
||||||
|
SECRET_CACHE.put(compoundKey, secret);
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache negative result
|
||||||
|
SECRET_CACHE.put(compoundKey, null);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string containing the txid of the transaction that funded supplied <tt>p2shAddress</tt>
|
||||||
|
* We have to do this in a bit of a roundabout way due to the Pirate Light Client server omitting
|
||||||
|
* transaction hashes from the raw transaction data.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
public static String getFundingTxid(BitcoinyBlockchainProvider blockchain, String p2shAddress) throws ForeignBlockchainException {
|
||||||
|
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
|
||||||
|
// HASH160(redeem script) for this p2shAddress
|
||||||
|
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
|
||||||
|
|
||||||
|
|
||||||
|
// Firstly look for an unspent output
|
||||||
|
|
||||||
|
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
|
||||||
|
List<UnspentOutput> unspentOutputs = blockchain.getUnspentOutputs(p2shAddress, false);
|
||||||
|
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||||
|
|
||||||
|
if (!Arrays.equals(ourScriptPubKey, unspentOutput.script)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HashCode.fromBytes(unspentOutput.hash).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// No valid unspent outputs, so must be already spent...
|
||||||
|
|
||||||
|
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
|
||||||
|
List<BitcoinyTransaction> transactions = blockchain.getAddressBitcoinyTransactions(p2shAddress, BitcoinyBlockchainProvider.EXCLUDE_UNCONFIRMED);
|
||||||
|
|
||||||
|
// Sort by confirmed first, followed by ascending height
|
||||||
|
transactions.sort(BitcoinyTransaction.CONFIRMED_FIRST.thenComparing(BitcoinyTransaction::getHeight));
|
||||||
|
|
||||||
|
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
|
||||||
|
|
||||||
|
// Acceptable funding is one transaction output, so we're expecting only one input
|
||||||
|
if (bitcoinyTransaction.inputs.size() != 1)
|
||||||
|
// Wrong number of inputs
|
||||||
|
continue;
|
||||||
|
|
||||||
|
String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
|
||||||
|
|
||||||
|
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
|
||||||
|
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
|
||||||
|
// Not valid chunks for our form of HTLC
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Last chunk is redeem script
|
||||||
|
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
|
||||||
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
|
||||||
|
// Not spending our specific HTLC redeem script
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return bitcoinyTransaction.inputs.get(0).outputTxHash;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string containing the unspent txid of the transaction that funded supplied <tt>p2shAddress</tt>
|
||||||
|
* and is at least the value specified in <tt>minimumAmount</tt>
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
public static String getUnspentFundingTxid(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException {
|
||||||
|
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
|
||||||
|
|
||||||
|
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
|
||||||
|
List<UnspentOutput> unspentOutputs = blockchain.getUnspentOutputs(p2shAddress, false);
|
||||||
|
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||||
|
|
||||||
|
if (!Arrays.equals(ourScriptPubKey, unspentOutput.script)) {
|
||||||
|
// Not funding our specific HTLC script hash
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unspentOutput.value < minimumAmount) {
|
||||||
|
// Not funding the required amount
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HashCode.fromBytes(unspentOutput.hash).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// No valid unspent outputs, so must be already spent
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTLC status, given P2SH address and expected redeem/refund amount
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
public static Status determineHtlcStatus(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException {
|
||||||
|
String compoundKey = String.format("%s-%s-%d", blockchain.getNetId(), p2shAddress, System.currentTimeMillis() / CACHE_TIMEOUT);
|
||||||
|
|
||||||
|
Status cachedStatus = STATUS_CACHE.getOrDefault(compoundKey, null);
|
||||||
|
if (cachedStatus != null)
|
||||||
|
return cachedStatus;
|
||||||
|
|
||||||
|
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
|
||||||
|
|
||||||
|
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
|
||||||
|
List<BitcoinyTransaction> transactions = blockchain.getAddressBitcoinyTransactions(p2shAddress, BitcoinyBlockchainProvider.EXCLUDE_UNCONFIRMED);
|
||||||
|
|
||||||
|
// Sort by confirmed first, followed by ascending height
|
||||||
|
transactions.sort(BitcoinyTransaction.CONFIRMED_FIRST.thenComparing(BitcoinyTransaction::getHeight));
|
||||||
|
|
||||||
|
// Transaction cache
|
||||||
|
//Map<String, BitcoinyTransaction> transactionsByHash = new HashMap<>();
|
||||||
|
// HASH160(redeem script) for this p2shAddress
|
||||||
|
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
|
||||||
|
|
||||||
|
// Check for spends first, caching full transaction info as we progress just in case we don't return in this loop
|
||||||
|
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
|
||||||
|
|
||||||
|
// Cache for possible later reuse
|
||||||
|
// transactionsByHash.put(transactionInfo.txHash, bitcoinyTransaction);
|
||||||
|
|
||||||
|
// Acceptable funding is one transaction output, so we're expecting only one input
|
||||||
|
if (bitcoinyTransaction.inputs.size() != 1)
|
||||||
|
// Wrong number of inputs
|
||||||
|
continue;
|
||||||
|
|
||||||
|
String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
|
||||||
|
|
||||||
|
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
|
||||||
|
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
|
||||||
|
// Not valid chunks for our form of HTLC
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Last chunk is redeem script
|
||||||
|
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
|
||||||
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
|
||||||
|
// Not spending our specific HTLC redeem script
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (scriptSigChunks.size() == 4)
|
||||||
|
// If we have 4 chunks, then secret is present, hence redeem
|
||||||
|
cachedStatus = bitcoinyTransaction.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED;
|
||||||
|
else
|
||||||
|
cachedStatus = bitcoinyTransaction.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED;
|
||||||
|
|
||||||
|
STATUS_CACHE.put(compoundKey, cachedStatus);
|
||||||
|
return cachedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ourScriptPubKeyHex = HashCode.fromBytes(ourScriptPubKey).toString();
|
||||||
|
|
||||||
|
// Check for funding
|
||||||
|
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
|
||||||
|
if (bitcoinyTransaction == null)
|
||||||
|
// Should be present in map!
|
||||||
|
throw new ForeignBlockchainException("Cached Bitcoin transaction now missing?");
|
||||||
|
|
||||||
|
// Check outputs for our specific P2SH
|
||||||
|
for (BitcoinyTransaction.Output output : bitcoinyTransaction.outputs) {
|
||||||
|
// Check amount
|
||||||
|
if (output.value < minimumAmount)
|
||||||
|
// Output amount too small (not taking fees into account)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
String scriptPubKeyHex = output.scriptPubKey;
|
||||||
|
if (!scriptPubKeyHex.equals(ourScriptPubKeyHex))
|
||||||
|
// Not funding our specific P2SH
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cachedStatus = bitcoinyTransaction.height == 0 ? Status.FUNDING_IN_PROGRESS : Status.FUNDED;
|
||||||
|
STATUS_CACHE.put(compoundKey, cachedStatus);
|
||||||
|
return cachedStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedStatus = Status.UNFUNDED;
|
||||||
|
STATUS_CACHE.put(compoundKey, cachedStatus);
|
||||||
|
return cachedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<byte[]> extractScriptSigChunks(byte[] scriptSigBytes) {
|
||||||
|
List<byte[]> chunks = new ArrayList<>();
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
int previousOffset = 0;
|
||||||
|
while (offset < scriptSigBytes.length) {
|
||||||
|
byte pushOp = scriptSigBytes[offset++];
|
||||||
|
|
||||||
|
if (pushOp < 0 || pushOp > 0x4c)
|
||||||
|
// Unacceptable OP
|
||||||
|
return Collections.emptyList();
|
||||||
|
|
||||||
|
// Special treatment for OP_PUSHDATA1
|
||||||
|
if (pushOp == 0x4c) {
|
||||||
|
if (offset >= scriptSigBytes.length)
|
||||||
|
// Run out of scriptSig bytes?
|
||||||
|
return Collections.emptyList();
|
||||||
|
|
||||||
|
pushOp = scriptSigBytes[offset++];
|
||||||
|
}
|
||||||
|
|
||||||
|
previousOffset = offset;
|
||||||
|
offset += Byte.toUnsignedInt(pushOp);
|
||||||
|
|
||||||
|
byte[] chunk = Arrays.copyOfRange(scriptSigBytes, previousOffset, offset);
|
||||||
|
chunks.add(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] addressToScriptPubKey(String p2shAddress) {
|
||||||
|
// We want the HASH160 part of the P2SH address
|
||||||
|
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
|
||||||
|
|
||||||
|
byte[] scriptPubKey = new byte[1 + 1 + 20 + 1];
|
||||||
|
scriptPubKey[0x00] = (byte) 0xa9; /* OP_HASH160 */
|
||||||
|
scriptPubKey[0x01] = (byte) 0x14; /* PUSH 0x14 bytes */
|
||||||
|
System.arraycopy(p2shAddressBytes, 1, scriptPubKey, 2, 0x14);
|
||||||
|
scriptPubKey[0x16] = (byte) 0x87; /* OP_EQUAL */
|
||||||
|
|
||||||
|
return scriptPubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] addressToRedeemScriptHash(String p2shAddress) {
|
||||||
|
// We want the HASH160 part of the P2SH address
|
||||||
|
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
|
||||||
|
|
||||||
|
return Arrays.copyOfRange(p2shAddressBytes, 1, 1 + 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
649
src/main/java/org/qortal/crosschain/PirateLightClient.java
Normal file
649
src/main/java/org/qortal/crosschain/PirateLightClient.java
Normal file
@ -0,0 +1,649 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.rpc.CompactFormats.*;
|
||||||
|
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc;
|
||||||
|
import cash.z.wallet.sdk.rpc.Service;
|
||||||
|
import cash.z.wallet.sdk.rpc.Service.*;
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import io.grpc.ManagedChannel;
|
||||||
|
import io.grpc.ManagedChannelBuilder;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.json.simple.JSONArray;
|
||||||
|
import org.json.simple.JSONObject;
|
||||||
|
import org.json.simple.parser.JSONParser;
|
||||||
|
import org.json.simple.parser.ParseException;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/** Pirate Chain network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
|
||||||
|
public class PirateLightClient extends BitcoinyBlockchainProvider {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(PirateLightClient.class);
|
||||||
|
private static final Random RANDOM = new Random();
|
||||||
|
|
||||||
|
private static final int RESPONSE_TIME_READINGS = 5;
|
||||||
|
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
|
||||||
|
|
||||||
|
public static class Server {
|
||||||
|
String hostname;
|
||||||
|
|
||||||
|
public enum ConnectionType { TCP, SSL }
|
||||||
|
ConnectionType connectionType;
|
||||||
|
|
||||||
|
int port;
|
||||||
|
private List<Long> responseTimes = new ArrayList<>();
|
||||||
|
|
||||||
|
public Server(String hostname, ConnectionType connectionType, int port) {
|
||||||
|
this.hostname = hostname;
|
||||||
|
this.connectionType = connectionType;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addResponseTime(long responseTime) {
|
||||||
|
while (this.responseTimes.size() > RESPONSE_TIME_READINGS) {
|
||||||
|
this.responseTimes.remove(0);
|
||||||
|
}
|
||||||
|
this.responseTimes.add(responseTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long averageResponseTime() {
|
||||||
|
if (this.responseTimes.size() < RESPONSE_TIME_READINGS) {
|
||||||
|
// Not enough readings yet
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
OptionalDouble average = this.responseTimes.stream().mapToDouble(a -> a).average();
|
||||||
|
if (average.isPresent()) {
|
||||||
|
return Double.valueOf(average.getAsDouble()).longValue();
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (other == this)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!(other instanceof Server))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Server otherServer = (Server) other;
|
||||||
|
|
||||||
|
return this.connectionType == otherServer.connectionType
|
||||||
|
&& this.port == otherServer.port
|
||||||
|
&& this.hostname.equals(otherServer.hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.hostname.hashCode() ^ this.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private Set<Server> servers = new HashSet<>();
|
||||||
|
private List<Server> remainingServers = new ArrayList<>();
|
||||||
|
private Set<Server> uselessServers = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
|
private final String netId;
|
||||||
|
private final String expectedGenesisHash;
|
||||||
|
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
|
||||||
|
private Bitcoiny blockchain;
|
||||||
|
|
||||||
|
private final Object serverLock = new Object();
|
||||||
|
private Server currentServer;
|
||||||
|
private ManagedChannel channel;
|
||||||
|
private int nextId = 1;
|
||||||
|
|
||||||
|
private static final int TX_CACHE_SIZE = 1000;
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
private final Map<String, BitcoinyTransaction> transactionCache = Collections.synchronizedMap(new LinkedHashMap<>(TX_CACHE_SIZE + 1, 0.75F, true) {
|
||||||
|
// This method is called just after a new entry has been added
|
||||||
|
@Override
|
||||||
|
public boolean removeEldestEntry(Map.Entry<String, BitcoinyTransaction> eldest) {
|
||||||
|
return size() > TX_CACHE_SIZE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
public PirateLightClient(String netId, String genesisHash, Collection<Server> initialServerList, Map<Server.ConnectionType, Integer> defaultPorts) {
|
||||||
|
this.netId = netId;
|
||||||
|
this.expectedGenesisHash = genesisHash;
|
||||||
|
this.servers.addAll(initialServerList);
|
||||||
|
this.defaultPorts.putAll(defaultPorts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods for use by other classes
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockchain(Bitcoiny blockchain) {
|
||||||
|
this.blockchain = blockchain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNetId() {
|
||||||
|
return this.netId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current blockchain height.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getCurrentHeight() throws ForeignBlockchainException {
|
||||||
|
BlockID latestBlock = this.getCompactTxStreamerStub().getLatestBlock(null);
|
||||||
|
|
||||||
|
if (!(latestBlock instanceof BlockID))
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getLatestBlock gRPC");
|
||||||
|
|
||||||
|
return (int)latestBlock.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of compact blocks, starting from <tt>startHeight</tt> inclusive.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
|
||||||
|
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
|
||||||
|
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
|
||||||
|
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||||
|
|
||||||
|
Iterator<CompactBlock> blocksIterator = this.getCompactTxStreamerStub().getBlockRange(range);
|
||||||
|
|
||||||
|
// Map from Iterator to List
|
||||||
|
List<CompactBlock> blocks = new ArrayList<>();
|
||||||
|
blocksIterator.forEachRemaining(blocks::add);
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException {
|
||||||
|
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
|
||||||
|
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
|
||||||
|
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||||
|
|
||||||
|
Iterator<CompactBlock> blocks = this.getCompactTxStreamerStub().getBlockRange(range);
|
||||||
|
|
||||||
|
List<byte[]> rawBlockHeaders = new ArrayList<>();
|
||||||
|
|
||||||
|
while (blocks.hasNext()) {
|
||||||
|
CompactBlock block = blocks.next();
|
||||||
|
|
||||||
|
if (block.getHeader() == null) {
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getBlockRange gRPC");
|
||||||
|
}
|
||||||
|
|
||||||
|
rawBlockHeaders.add(block.getHeader().toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawBlockHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of raw block timestamps, starting from <tt>startHeight</tt> inclusive.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException {
|
||||||
|
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
|
||||||
|
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
|
||||||
|
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||||
|
|
||||||
|
Iterator<CompactBlock> blocks = this.getCompactTxStreamerStub().getBlockRange(range);
|
||||||
|
|
||||||
|
List<Long> rawBlockTimestamps = new ArrayList<>();
|
||||||
|
|
||||||
|
while (blocks.hasNext()) {
|
||||||
|
CompactBlock block = blocks.next();
|
||||||
|
|
||||||
|
if (block.getTime() <= 0) {
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getBlockRange gRPC");
|
||||||
|
}
|
||||||
|
|
||||||
|
rawBlockTimestamps.add(Long.valueOf(block.getTime()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawBlockTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns confirmed balance, based on passed payment script.
|
||||||
|
* <p>
|
||||||
|
* @return confirmed balance, or zero if script unknown
|
||||||
|
* @throws ForeignBlockchainException if there was an error
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getConfirmedBalance(byte[] script) throws ForeignBlockchainException {
|
||||||
|
throw new ForeignBlockchainException("getConfirmedBalance not yet implemented for Pirate Chain");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns confirmed balance, based on passed base58 encoded address.
|
||||||
|
* <p>
|
||||||
|
* @return confirmed balance, or zero if address unknown
|
||||||
|
* @throws ForeignBlockchainException if there was an error
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException {
|
||||||
|
AddressList addressList = AddressList.newBuilder().addAddresses(base58Address).build();
|
||||||
|
Balance balance = this.getCompactTxStreamerStub().getTaddressBalance(addressList);
|
||||||
|
|
||||||
|
if (!(balance instanceof Balance))
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getConfirmedAddressBalance gRPC");
|
||||||
|
|
||||||
|
return balance.getValueZat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of unspent outputs pertaining to passed address.
|
||||||
|
* <p>
|
||||||
|
* @return list of unspent outputs, or empty list if address unknown
|
||||||
|
* @throws ForeignBlockchainException if there was an error.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||||
|
GetAddressUtxosArg getAddressUtxosArg = GetAddressUtxosArg.newBuilder().addAddresses(address).build();
|
||||||
|
GetAddressUtxosReplyList replyList = this.getCompactTxStreamerStub().getAddressUtxos(getAddressUtxosArg);
|
||||||
|
|
||||||
|
if (!(replyList instanceof GetAddressUtxosReplyList))
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getUnspentOutputs gRPC");
|
||||||
|
|
||||||
|
List<GetAddressUtxosReply> unspentList = replyList.getAddressUtxosList();
|
||||||
|
if (unspentList == null)
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getUnspentOutputs gRPC");
|
||||||
|
|
||||||
|
List<UnspentOutput> unspentOutputs = new ArrayList<>();
|
||||||
|
for (GetAddressUtxosReply unspent : unspentList) {
|
||||||
|
|
||||||
|
int height = (int)unspent.getHeight();
|
||||||
|
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
|
||||||
|
if (!includeUnconfirmed && height <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] txHash = unspent.getTxid().toByteArray();
|
||||||
|
int outputIndex = unspent.getIndex();
|
||||||
|
long value = unspent.getValueZat();
|
||||||
|
byte[] script = unspent.getScript().toByteArray();
|
||||||
|
String addressRes = unspent.getAddress();
|
||||||
|
|
||||||
|
unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value, script, addressRes));
|
||||||
|
}
|
||||||
|
|
||||||
|
return unspentOutputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of unspent outputs pertaining to passed payment script.
|
||||||
|
* <p>
|
||||||
|
* @return list of unspent outputs, or empty list if script unknown
|
||||||
|
* @throws ForeignBlockchainException if there was an error.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||||
|
String address = this.blockchain.deriveP2shAddress(script);
|
||||||
|
return this.getUnspentOutputs(address, includeUnconfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns raw transaction for passed transaction hash.
|
||||||
|
* <p>
|
||||||
|
* NOTE: Do not mutate returned byte[]!
|
||||||
|
*
|
||||||
|
* @throws ForeignBlockchainException.NotFoundException if transaction not found
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] getRawTransaction(String txHash) throws ForeignBlockchainException {
|
||||||
|
return getRawTransaction(HashCode.fromString(txHash).asBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns raw transaction for passed transaction hash.
|
||||||
|
* <p>
|
||||||
|
* NOTE: Do not mutate returned byte[]!
|
||||||
|
*
|
||||||
|
* @throws ForeignBlockchainException.NotFoundException if transaction not found
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException {
|
||||||
|
ByteString byteString = ByteString.copyFrom(txHash);
|
||||||
|
TxFilter txFilter = TxFilter.newBuilder().setHash(byteString).build();
|
||||||
|
RawTransaction rawTransaction = this.getCompactTxStreamerStub().getTransaction(txFilter);
|
||||||
|
|
||||||
|
if (!(rawTransaction instanceof RawTransaction))
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getTransaction gRPC");
|
||||||
|
|
||||||
|
return rawTransaction.getData().toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns transaction info for passed transaction hash.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException.NotFoundException if transaction not found
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
|
||||||
|
// Check cache first
|
||||||
|
BitcoinyTransaction transaction = transactionCache.get(txHash);
|
||||||
|
if (transaction != null)
|
||||||
|
return transaction;
|
||||||
|
|
||||||
|
ByteString byteString = ByteString.copyFrom(HashCode.fromString(txHash).asBytes());
|
||||||
|
TxFilter txFilter = TxFilter.newBuilder().setHash(byteString).build();
|
||||||
|
RawTransaction rawTransaction = this.getCompactTxStreamerStub().getTransaction(txFilter);
|
||||||
|
|
||||||
|
if (!(rawTransaction instanceof RawTransaction))
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getTransaction gRPC");
|
||||||
|
|
||||||
|
byte[] transactionData = rawTransaction.getData().toByteArray();
|
||||||
|
String transactionDataString = HashCode.fromBytes(transactionData).toString();
|
||||||
|
|
||||||
|
JSONParser parser = new JSONParser();
|
||||||
|
JSONObject transactionJson;
|
||||||
|
try {
|
||||||
|
transactionJson = (JSONObject) parser.parse(transactionDataString);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Expected JSON string from Pirate Chain getTransaction gRPC");
|
||||||
|
}
|
||||||
|
|
||||||
|
Object inputsObj = transactionJson.get("vin");
|
||||||
|
if (!(inputsObj instanceof JSONArray))
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vin' from Pirate Chain getTransaction gRPC");
|
||||||
|
|
||||||
|
Object outputsObj = transactionJson.get("vout");
|
||||||
|
if (!(outputsObj instanceof JSONArray))
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vout' from Pirate Chain getTransaction gRPC");
|
||||||
|
|
||||||
|
try {
|
||||||
|
int size = ((Long) transactionJson.get("size")).intValue();
|
||||||
|
int locktime = ((Long) transactionJson.get("locktime")).intValue();
|
||||||
|
|
||||||
|
// Timestamp might not be present, e.g. for unconfirmed transaction
|
||||||
|
Object timeObj = transactionJson.get("time");
|
||||||
|
Integer timestamp = timeObj != null
|
||||||
|
? ((Long) timeObj).intValue()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
|
||||||
|
for (Object inputObj : (JSONArray) inputsObj) {
|
||||||
|
JSONObject inputJson = (JSONObject) inputObj;
|
||||||
|
|
||||||
|
String scriptSig = (String) ((JSONObject) inputJson.get("scriptSig")).get("hex");
|
||||||
|
int sequence = ((Long) inputJson.get("sequence")).intValue();
|
||||||
|
String outputTxHash = (String) inputJson.get("txid");
|
||||||
|
int outputVout = ((Long) inputJson.get("vout")).intValue();
|
||||||
|
|
||||||
|
inputs.add(new BitcoinyTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
|
||||||
|
for (Object outputObj : (JSONArray) outputsObj) {
|
||||||
|
JSONObject outputJson = (JSONObject) outputObj;
|
||||||
|
|
||||||
|
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
|
||||||
|
long value = BigDecimal.valueOf((Double) outputJson.get("value")).setScale(8).unscaledValue().longValue();
|
||||||
|
|
||||||
|
// address too, if present in the "addresses" array
|
||||||
|
List<String> addresses = null;
|
||||||
|
Object addressesObj = ((JSONObject) outputJson.get("scriptPubKey")).get("addresses");
|
||||||
|
if (addressesObj instanceof JSONArray) {
|
||||||
|
addresses = new ArrayList<>();
|
||||||
|
for (Object addressObj : (JSONArray) addressesObj) {
|
||||||
|
addresses.add((String) addressObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// some peers return a single "address" string
|
||||||
|
Object addressObj = ((JSONObject) outputJson.get("scriptPubKey")).get("address");
|
||||||
|
if (addressObj instanceof String) {
|
||||||
|
if (addresses == null) {
|
||||||
|
addresses = new ArrayList<>();
|
||||||
|
}
|
||||||
|
addresses.add((String) addressObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the purposes of Qortal we require all outputs to contain addresses
|
||||||
|
// Some servers omit this info, causing problems down the line with balance calculations
|
||||||
|
// Update: it turns out that they were just using a different key - "address" instead of "addresses"
|
||||||
|
// The code below can remain in place, just in case a peer returns a missing address in the future
|
||||||
|
if (addresses == null || addresses.isEmpty()) {
|
||||||
|
if (this.currentServer != null) {
|
||||||
|
this.uselessServers.add(this.currentServer);
|
||||||
|
this.closeServer(this.currentServer);
|
||||||
|
}
|
||||||
|
LOGGER.info("No output addresses returned for transaction {}", txHash);
|
||||||
|
throw new ForeignBlockchainException(String.format("No output addresses returned for transaction %s", txHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses));
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction = new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
|
||||||
|
|
||||||
|
// Save into cache
|
||||||
|
transactionCache.put(txHash, transaction);
|
||||||
|
|
||||||
|
return transaction;
|
||||||
|
} catch (NullPointerException | ClassCastException e) {
|
||||||
|
// Unexpected / invalid response from ElectrumX server
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected JSON format from Pirate Chain getTransaction gRPC");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of transactions, relating to passed payment script.
|
||||||
|
* <p>
|
||||||
|
* @return list of related transactions, or empty list if script unknown
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||||
|
// FUTURE: implement this if needed. Probably not very useful for private blockchains.
|
||||||
|
throw new ForeignBlockchainException("getAddressTransactions not yet implemented for Pirate Chain");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||||
|
try {
|
||||||
|
// Firstly we need to get the latest block
|
||||||
|
int defaultBirthday = Settings.getInstance().getArrrDefaultBirthday();
|
||||||
|
BlockID endBlock = this.getCompactTxStreamerStub().getLatestBlock(null);
|
||||||
|
BlockID startBlock = BlockID.newBuilder().setHeight(defaultBirthday).build();
|
||||||
|
BlockRange blockRange = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
|
||||||
|
|
||||||
|
TransparentAddressBlockFilter blockFilter = TransparentAddressBlockFilter.newBuilder()
|
||||||
|
.setAddress(address)
|
||||||
|
.setRange(blockRange)
|
||||||
|
.build();
|
||||||
|
Iterator<Service.RawTransaction> transactionIterator = this.getCompactTxStreamerStub().getTaddressTxids(blockFilter);
|
||||||
|
|
||||||
|
// Map from Iterator to List
|
||||||
|
List<RawTransaction> rawTransactions = new ArrayList<>();
|
||||||
|
transactionIterator.forEachRemaining(rawTransactions::add);
|
||||||
|
|
||||||
|
List<BitcoinyTransaction> transactions = new ArrayList<>();
|
||||||
|
|
||||||
|
for (RawTransaction rawTransaction : rawTransactions) {
|
||||||
|
|
||||||
|
Long height = rawTransaction.getHeight();
|
||||||
|
if (!includeUnconfirmed && (height == null || height == 0))
|
||||||
|
// We only want confirmed transactions
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] transactionData = rawTransaction.getData().toByteArray();
|
||||||
|
String transactionDataHex = HashCode.fromBytes(transactionData).toString();
|
||||||
|
BitcoinyTransaction bitcoinyTransaction = PirateChain.deserializeRawTransaction(transactionDataHex);
|
||||||
|
bitcoinyTransaction.height = height.intValue();
|
||||||
|
transactions.add(bitcoinyTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
catch (RuntimeException | TransformationException e) {
|
||||||
|
throw new ForeignBlockchainException(String.format("Unable to get transactions for address %s: %s", address, e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts raw transaction to network.
|
||||||
|
* <p>
|
||||||
|
* @throws ForeignBlockchainException if error occurs
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void broadcastTransaction(byte[] transactionBytes) throws ForeignBlockchainException {
|
||||||
|
ByteString byteString = ByteString.copyFrom(transactionBytes);
|
||||||
|
RawTransaction rawTransaction = RawTransaction.newBuilder().setData(byteString).build();
|
||||||
|
SendResponse sendResponse = this.getCompactTxStreamerStub().sendTransaction(rawTransaction);
|
||||||
|
|
||||||
|
if (!(sendResponse instanceof SendResponse))
|
||||||
|
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain broadcastTransaction gRPC");
|
||||||
|
|
||||||
|
if (sendResponse.getErrorCode() != 0)
|
||||||
|
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error code from Pirate Chain broadcastTransaction gRPC: %d", sendResponse.getErrorCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class-private utility methods
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs RPC call, with automatic reconnection to different server if needed.
|
||||||
|
* <p>
|
||||||
|
* @return "result" object from within JSON output
|
||||||
|
* @throws ForeignBlockchainException if server returns error or something goes wrong
|
||||||
|
*/
|
||||||
|
private CompactTxStreamerGrpc.CompactTxStreamerBlockingStub getCompactTxStreamerStub() throws ForeignBlockchainException {
|
||||||
|
synchronized (this.serverLock) {
|
||||||
|
if (this.remainingServers.isEmpty())
|
||||||
|
this.remainingServers.addAll(this.servers);
|
||||||
|
|
||||||
|
while (haveConnection()) {
|
||||||
|
// If we have more servers and the last one replied slowly, try another
|
||||||
|
if (!this.remainingServers.isEmpty()) {
|
||||||
|
long averageResponseTime = this.currentServer.averageResponseTime();
|
||||||
|
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
|
||||||
|
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname);
|
||||||
|
this.closeServer();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompactTxStreamerGrpc.newBlockingStub(this.channel);
|
||||||
|
|
||||||
|
// // Didn't work, try another server...
|
||||||
|
// this.closeServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed to perform RPC - maybe lack of servers?
|
||||||
|
LOGGER.info("Error: No connected Pirate Light servers when trying to make RPC call");
|
||||||
|
throw new ForeignBlockchainException.NetworkException("No connected Pirate Light servers when trying to make RPC call");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if we have, or create, a connection to an ElectrumX server. */
|
||||||
|
private boolean haveConnection() throws ForeignBlockchainException {
|
||||||
|
if (this.currentServer != null && this.channel != null && !this.channel.isShutdown())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
while (!this.remainingServers.isEmpty()) {
|
||||||
|
Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
|
||||||
|
LOGGER.trace(() -> String.format("Connecting to %s", server));
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.channel = ManagedChannelBuilder.forAddress(server.hostname, server.port).build();
|
||||||
|
|
||||||
|
CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel);
|
||||||
|
LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build());
|
||||||
|
|
||||||
|
if (lightdInfo == null || lightdInfo.getBlockHeight() <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// TODO: find a way to verify that the server is using the expected chain
|
||||||
|
|
||||||
|
// if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
|
||||||
|
// continue;
|
||||||
|
|
||||||
|
// if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
|
||||||
|
// continue;
|
||||||
|
|
||||||
|
LOGGER.debug(() -> String.format("Connected to %s", server));
|
||||||
|
this.currentServer = server;
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Didn't work, try another server...
|
||||||
|
closeServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes connection to <tt>server</tt> if it is currently connected server.
|
||||||
|
* @param server
|
||||||
|
*/
|
||||||
|
private void closeServer(Server server) {
|
||||||
|
synchronized (this.serverLock) {
|
||||||
|
if (this.currentServer == null || !this.currentServer.equals(server) || this.channel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the gRPC managed-channel if not shut down already.
|
||||||
|
if (!this.channel.isShutdown()) {
|
||||||
|
try {
|
||||||
|
this.channel.shutdown();
|
||||||
|
if (!this.channel.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||||
|
LOGGER.warn("Timed out gracefully shutting down connection: {}. ", this.channel);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Unexpected exception while waiting for channel termination", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forceful shut down if still not terminated.
|
||||||
|
if (!this.channel.isTerminated()) {
|
||||||
|
try {
|
||||||
|
this.channel.shutdownNow();
|
||||||
|
if (!this.channel.awaitTermination(15, TimeUnit.SECONDS)) {
|
||||||
|
LOGGER.warn("Timed out forcefully shutting down connection: {}. ", this.channel);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Unexpected exception while waiting for channel termination", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.channel = null;
|
||||||
|
this.currentServer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Closes connection to currently connected server (if any). */
|
||||||
|
private void closeServer() {
|
||||||
|
synchronized (this.serverLock) {
|
||||||
|
this.closeServer(this.currentServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
409
src/main/java/org/qortal/crosschain/PirateWallet.java
Normal file
409
src/main/java/org/qortal/crosschain/PirateWallet.java
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import com.rust.litewalletjni.LiteWalletJni;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bouncycastle.util.encoders.Base64;
|
||||||
|
import org.bouncycastle.util.encoders.DecoderException;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.qortal.controller.PirateChainWalletController;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class PirateWallet {
|
||||||
|
|
||||||
|
protected static final Logger LOGGER = LogManager.getLogger(PirateWallet.class);
|
||||||
|
|
||||||
|
private byte[] entropyBytes;
|
||||||
|
private final boolean isNullSeedWallet;
|
||||||
|
private String seedPhrase;
|
||||||
|
private boolean ready = false;
|
||||||
|
|
||||||
|
private String params;
|
||||||
|
private String saplingOutput64;
|
||||||
|
private String saplingSpend64;
|
||||||
|
|
||||||
|
private final static String COIN_PARAMS_FILENAME = "coinparams.json";
|
||||||
|
private final static String SAPLING_OUTPUT_FILENAME = "saplingoutput_base64";
|
||||||
|
private final static String SAPLING_SPEND_FILENAME = "saplingspend_base64";
|
||||||
|
|
||||||
|
public PirateWallet(byte[] entropyBytes, boolean isNullSeedWallet) throws IOException {
|
||||||
|
this.entropyBytes = entropyBytes;
|
||||||
|
this.isNullSeedWallet = isNullSeedWallet;
|
||||||
|
|
||||||
|
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
|
||||||
|
if (!Files.exists(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.params = Files.readString(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME));
|
||||||
|
this.saplingOutput64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_OUTPUT_FILENAME));
|
||||||
|
this.saplingSpend64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_SPEND_FILENAME));
|
||||||
|
|
||||||
|
this.ready = this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean initialize() {
|
||||||
|
try {
|
||||||
|
LiteWalletJni.initlogging();
|
||||||
|
|
||||||
|
if (this.entropyBytes == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a random server
|
||||||
|
PirateLightClient.Server server = this.getRandomServer();
|
||||||
|
String serverUri = String.format("https://%s:%d/", server.hostname, server.port);
|
||||||
|
|
||||||
|
// Pirate library uses base64 encoding
|
||||||
|
String entropy64 = Base64.toBase64String(this.entropyBytes);
|
||||||
|
|
||||||
|
// Derive seed phrase from entropy bytes
|
||||||
|
String inputSeedResponse = LiteWalletJni.getseedphrasefromentropyb64(entropy64);
|
||||||
|
JSONObject inputSeedJson = new JSONObject(inputSeedResponse);
|
||||||
|
String inputSeedPhrase = null;
|
||||||
|
if (inputSeedJson.has("seedPhrase")) {
|
||||||
|
inputSeedPhrase = inputSeedJson.getString("seedPhrase");
|
||||||
|
}
|
||||||
|
|
||||||
|
String wallet = this.load();
|
||||||
|
if (wallet == null) {
|
||||||
|
// Wallet doesn't exist, so create a new one
|
||||||
|
|
||||||
|
int birthday = Settings.getInstance().getArrrDefaultBirthday();
|
||||||
|
if (this.isNullSeedWallet) {
|
||||||
|
try {
|
||||||
|
// Attempt to set birthday to the current block for null seed wallets
|
||||||
|
birthday = PirateChain.getInstance().blockchainProvider.getCurrentHeight();
|
||||||
|
}
|
||||||
|
catch (ForeignBlockchainException e) {
|
||||||
|
// Use the default height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize new wallet
|
||||||
|
String birthdayString = String.format("%d", birthday);
|
||||||
|
String outputSeedResponse = LiteWalletJni.initfromseed(serverUri, this.params, inputSeedPhrase, birthdayString, this.saplingOutput64, this.saplingSpend64); // Thread-safe.
|
||||||
|
JSONObject outputSeedJson = new JSONObject(outputSeedResponse);
|
||||||
|
String outputSeedPhrase = null;
|
||||||
|
if (outputSeedJson.has("seed")) {
|
||||||
|
outputSeedPhrase = outputSeedJson.getString("seed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure seed phrase in response matches supplied seed phrase
|
||||||
|
if (inputSeedPhrase == null || !Objects.equals(inputSeedPhrase, outputSeedPhrase)) {
|
||||||
|
LOGGER.info("Unable to initialize Pirate Chain wallet: seed phrases do not match, or are null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.seedPhrase = outputSeedPhrase;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Restore existing wallet
|
||||||
|
String response = LiteWalletJni.initfromb64(serverUri, params, wallet, saplingOutput64, saplingSpend64);
|
||||||
|
if (response != null && !response.contains("\"initalized\":true")) {
|
||||||
|
LOGGER.info("Unable to initialize Pirate Chain wallet at {}: {}", serverUri, response);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.seedPhrase = inputSeedPhrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we're able to communicate with the library
|
||||||
|
Integer ourHeight = this.getHeight();
|
||||||
|
return (ourHeight != null && ourHeight > 0);
|
||||||
|
|
||||||
|
} catch (IOException | JSONException | UnsatisfiedLinkError e) {
|
||||||
|
LOGGER.info("Unable to initialize Pirate Chain wallet: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReady() {
|
||||||
|
return this.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReady(boolean ready) {
|
||||||
|
this.ready = ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean entropyBytesEqual(byte[] testEntropyBytes) {
|
||||||
|
return Arrays.equals(testEntropyBytes, this.entropyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void encrypt() {
|
||||||
|
if (this.isEncrypted()) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String encryptionKey = this.getEncryptionKey();
|
||||||
|
if (encryptionKey == null) {
|
||||||
|
// Can't encrypt without a key
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.doEncrypt(encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decrypt() {
|
||||||
|
if (!this.isEncrypted()) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String encryptionKey = this.getEncryptionKey();
|
||||||
|
if (encryptionKey == null) {
|
||||||
|
// Can't encrypt without a key
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.doDecrypt(encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlock() {
|
||||||
|
if (!this.isEncrypted()) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String encryptionKey = this.getEncryptionKey();
|
||||||
|
if (encryptionKey == null) {
|
||||||
|
// Can't encrypt without a key
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.doUnlock(encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean save() throws IOException {
|
||||||
|
if (!isInitialized()) {
|
||||||
|
LOGGER.info("Error: can't save wallet, because no wallet it initialized");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.isNullSeedWallet()) {
|
||||||
|
// Don't save wallets that have a null seed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt first (will do nothing if already encrypted)
|
||||||
|
this.encrypt();
|
||||||
|
|
||||||
|
String wallet64 = LiteWalletJni.save();
|
||||||
|
byte[] wallet;
|
||||||
|
try {
|
||||||
|
wallet = Base64.decode(wallet64);
|
||||||
|
}
|
||||||
|
catch (DecoderException e) {
|
||||||
|
LOGGER.info("Unable to decode wallet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (wallet == null) {
|
||||||
|
LOGGER.info("Unable to save wallet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path walletPath = this.getCurrentWalletPath();
|
||||||
|
Files.createDirectories(walletPath.getParent());
|
||||||
|
Files.write(walletPath, wallet, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
LOGGER.debug("Saved Pirate Chain wallet");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String load() throws IOException {
|
||||||
|
if (this.isNullSeedWallet()) {
|
||||||
|
// Don't load wallets that have a null seed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Path walletPath = this.getCurrentWalletPath();
|
||||||
|
if (!Files.exists(walletPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] wallet = Files.readAllBytes(walletPath);
|
||||||
|
if (wallet == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String wallet64 = Base64.toBase64String(wallet);
|
||||||
|
return wallet64;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getEntropyHash58() {
|
||||||
|
if (this.entropyBytes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] entropyHash = Crypto.digest(this.entropyBytes);
|
||||||
|
return Base58.encode(entropyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSeedPhrase() {
|
||||||
|
return this.seedPhrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getEncryptionKey() {
|
||||||
|
if (this.entropyBytes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix the bytes with a (deterministic) string, to ensure that the resulting hash is different
|
||||||
|
String prefix = "ARRRWalletEncryption";
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
outputStream.write(prefix.getBytes(StandardCharsets.UTF_8));
|
||||||
|
outputStream.write(this.entropyBytes);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encryptionKeyHash = Crypto.digest(outputStream.toByteArray());
|
||||||
|
return Base58.encode(encryptionKeyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getCurrentWalletPath() {
|
||||||
|
String entropyHash58 = this.getEntropyHash58();
|
||||||
|
String filename = String.format("wallet-%s.dat", entropyHash58);
|
||||||
|
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return this.entropyBytes != null && this.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSynchronized() {
|
||||||
|
Integer height = this.getHeight();
|
||||||
|
Integer chainTip = this.getChainTip();
|
||||||
|
|
||||||
|
if (height == null || chainTip == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume synchronized if within 2 blocks of the chain tip
|
||||||
|
return height >= (chainTip - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// APIs
|
||||||
|
|
||||||
|
public Integer getHeight() {
|
||||||
|
String response = LiteWalletJni.execute("height", "");
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
if (json.has("height")) {
|
||||||
|
return json.getInt("height");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getChainTip() {
|
||||||
|
String response = LiteWalletJni.execute("info", "");
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
if (json.has("latest_block_height")) {
|
||||||
|
return json.getInt("latest_block_height");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNullSeedWallet() {
|
||||||
|
return this.isNullSeedWallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean isEncrypted() {
|
||||||
|
String response = LiteWalletJni.execute("encryptionstatus", "");
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
if (json.has("encrypted")) {
|
||||||
|
return json.getBoolean("encrypted");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean doEncrypt(String key) {
|
||||||
|
String response = LiteWalletJni.execute("encrypt", key);
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
String result = json.getString("result");
|
||||||
|
if (json.has("result")) {
|
||||||
|
return (Objects.equals(result, "success"));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean doDecrypt(String key) {
|
||||||
|
String response = LiteWalletJni.execute("decrypt", key);
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
String result = json.getString("result");
|
||||||
|
if (json.has("result")) {
|
||||||
|
return (Objects.equals(result, "success"));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean doUnlock(String key) {
|
||||||
|
String response = LiteWalletJni.execute("unlock", key);
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
String result = json.getString("result");
|
||||||
|
if (json.has("result")) {
|
||||||
|
return (Objects.equals(result, "success"));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWalletAddress() {
|
||||||
|
// Get balance, which also contains wallet addresses
|
||||||
|
String response = LiteWalletJni.execute("balance", "");
|
||||||
|
JSONObject json = new JSONObject(response);
|
||||||
|
String address = null;
|
||||||
|
|
||||||
|
if (json.has("z_addresses")) {
|
||||||
|
JSONArray z_addresses = json.getJSONArray("z_addresses");
|
||||||
|
|
||||||
|
if (z_addresses != null && !z_addresses.isEmpty()) {
|
||||||
|
JSONObject firstAddress = z_addresses.getJSONObject(0);
|
||||||
|
if (firstAddress.has("address")) {
|
||||||
|
address = firstAddress.getString("address");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPrivateKey() {
|
||||||
|
String response = LiteWalletJni.execute("export", "");
|
||||||
|
JSONArray addressesJson = new JSONArray(response);
|
||||||
|
if (!addressesJson.isEmpty()) {
|
||||||
|
JSONObject addressJson = addressesJson.getJSONObject(0);
|
||||||
|
if (addressJson.has("private_key")) {
|
||||||
|
//String address = addressJson.getString("address");
|
||||||
|
String privateKey = addressJson.getString("private_key");
|
||||||
|
//String viewingKey = addressJson.getString("viewing_key");
|
||||||
|
|
||||||
|
return privateKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PirateLightClient.Server getRandomServer() {
|
||||||
|
PirateChain.PirateChainNet pirateChainNet = Settings.getInstance().getPirateChainNet();
|
||||||
|
Collection<PirateLightClient.Server> servers = pirateChainNet.getServers();
|
||||||
|
Random random = new Random();
|
||||||
|
int index = random.nextInt(servers.size());
|
||||||
|
return (PirateLightClient.Server) servers.toArray()[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -138,6 +138,8 @@ public class Ravencoin extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(ravencoinNet.getParams());
|
Context bitcoinjContext = new Context(ravencoinNet.getParams());
|
||||||
|
|
||||||
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -7,11 +7,12 @@ import java.util.List;
|
|||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
public class SimpleTransaction {
|
public class SimpleTransaction {
|
||||||
private String txHash;
|
private String txHash;
|
||||||
private Integer timestamp;
|
private Long timestamp;
|
||||||
private long totalAmount;
|
private long totalAmount;
|
||||||
private long feeAmount;
|
private long feeAmount;
|
||||||
private List<Input> inputs;
|
private List<Input> inputs;
|
||||||
private List<Output> outputs;
|
private List<Output> outputs;
|
||||||
|
private String memo;
|
||||||
|
|
||||||
|
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@ -74,20 +75,21 @@ public class SimpleTransaction {
|
|||||||
public SimpleTransaction() {
|
public SimpleTransaction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public SimpleTransaction(String txHash, Integer timestamp, long totalAmount, long feeAmount, List<Input> inputs, List<Output> outputs) {
|
public SimpleTransaction(String txHash, Long timestamp, long totalAmount, long feeAmount, List<Input> inputs, List<Output> outputs, String memo) {
|
||||||
this.txHash = txHash;
|
this.txHash = txHash;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.totalAmount = totalAmount;
|
this.totalAmount = totalAmount;
|
||||||
this.feeAmount = feeAmount;
|
this.feeAmount = feeAmount;
|
||||||
this.inputs = inputs;
|
this.inputs = inputs;
|
||||||
this.outputs = outputs;
|
this.outputs = outputs;
|
||||||
|
this.memo = memo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTxHash() {
|
public String getTxHash() {
|
||||||
return txHash;
|
return txHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getTimestamp() {
|
public Long getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@ public enum SupportedBlockchain {
|
|||||||
|
|
||||||
LITECOIN(Arrays.asList(
|
LITECOIN(Arrays.asList(
|
||||||
Triple.valueOf(LitecoinACCTv1.NAME, LitecoinACCTv1.CODE_BYTES_HASH, LitecoinACCTv1::getInstance),
|
Triple.valueOf(LitecoinACCTv1.NAME, LitecoinACCTv1.CODE_BYTES_HASH, LitecoinACCTv1::getInstance),
|
||||||
Triple.valueOf(LitecoinACCTv2.NAME, LitecoinACCTv2.CODE_BYTES_HASH, LitecoinACCTv2::getInstance),
|
|
||||||
Triple.valueOf(LitecoinACCTv3.NAME, LitecoinACCTv3.CODE_BYTES_HASH, LitecoinACCTv3::getInstance)
|
Triple.valueOf(LitecoinACCTv3.NAME, LitecoinACCTv3.CODE_BYTES_HASH, LitecoinACCTv3::getInstance)
|
||||||
)) {
|
)) {
|
||||||
@Override
|
@Override
|
||||||
@ -45,7 +44,6 @@ public enum SupportedBlockchain {
|
|||||||
|
|
||||||
DOGECOIN(Arrays.asList(
|
DOGECOIN(Arrays.asList(
|
||||||
Triple.valueOf(DogecoinACCTv1.NAME, DogecoinACCTv1.CODE_BYTES_HASH, DogecoinACCTv1::getInstance),
|
Triple.valueOf(DogecoinACCTv1.NAME, DogecoinACCTv1.CODE_BYTES_HASH, DogecoinACCTv1::getInstance),
|
||||||
Triple.valueOf(DogecoinACCTv2.NAME, DogecoinACCTv2.CODE_BYTES_HASH, DogecoinACCTv2::getInstance),
|
|
||||||
Triple.valueOf(DogecoinACCTv3.NAME, DogecoinACCTv3.CODE_BYTES_HASH, DogecoinACCTv3::getInstance)
|
Triple.valueOf(DogecoinACCTv3.NAME, DogecoinACCTv3.CODE_BYTES_HASH, DogecoinACCTv3::getInstance)
|
||||||
)) {
|
)) {
|
||||||
@Override
|
@Override
|
||||||
@ -85,6 +83,20 @@ public enum SupportedBlockchain {
|
|||||||
public ACCT getLatestAcct() {
|
public ACCT getLatestAcct() {
|
||||||
return RavencoinACCTv3.getInstance();
|
return RavencoinACCTv3.getInstance();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
PIRATECHAIN(Arrays.asList(
|
||||||
|
Triple.valueOf(PirateChainACCTv3.NAME, PirateChainACCTv3.CODE_BYTES_HASH, PirateChainACCTv3::getInstance)
|
||||||
|
)) {
|
||||||
|
@Override
|
||||||
|
public ForeignBlockchain getInstance() {
|
||||||
|
return PirateChain.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ACCT getLatestAcct() {
|
||||||
|
return PirateChainACCTv3.getInstance();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
|
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
|
||||||
|
@ -7,10 +7,20 @@ public class UnspentOutput {
|
|||||||
public final int height;
|
public final int height;
|
||||||
public final long value;
|
public final long value;
|
||||||
|
|
||||||
public UnspentOutput(byte[] hash, int index, int height, long value) {
|
// Optional fields returned by Pirate Light Client server
|
||||||
|
public final byte[] script;
|
||||||
|
public final String address;
|
||||||
|
|
||||||
|
public UnspentOutput(byte[] hash, int index, int height, long value, byte[] script, String address) {
|
||||||
this.hash = hash;
|
this.hash = hash;
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
this.script = script;
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnspentOutput(byte[] hash, int index, int height, long value) {
|
||||||
|
this(hash, index, height, value, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,99 +0,0 @@
|
|||||||
package org.qortal.crypto;
|
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.Digest;
|
|
||||||
import org.bouncycastle.math.ec.rfc7748.X25519;
|
|
||||||
import org.bouncycastle.math.ec.rfc7748.X25519Field;
|
|
||||||
import org.bouncycastle.math.ec.rfc8032.Ed25519;
|
|
||||||
|
|
||||||
/** Additions to BouncyCastle providing Ed25519 to X25519 key conversion. */
|
|
||||||
public class BouncyCastle25519 {
|
|
||||||
|
|
||||||
private static final Class<?> pointAffineClass;
|
|
||||||
private static final Constructor<?> pointAffineCtor;
|
|
||||||
private static final Method decodePointVarMethod;
|
|
||||||
private static final Field yField;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
Class<?> ed25519Class = Ed25519.class;
|
|
||||||
pointAffineClass = Arrays.stream(ed25519Class.getDeclaredClasses()).filter(clazz -> clazz.getSimpleName().equals("PointAffine")).findFirst().get();
|
|
||||||
if (pointAffineClass == null)
|
|
||||||
throw new ClassNotFoundException("Can't locate PointExt inner class inside Ed25519");
|
|
||||||
|
|
||||||
decodePointVarMethod = ed25519Class.getDeclaredMethod("decodePointVar", byte[].class, int.class, boolean.class, pointAffineClass);
|
|
||||||
decodePointVarMethod.setAccessible(true);
|
|
||||||
|
|
||||||
pointAffineCtor = pointAffineClass.getDeclaredConstructors()[0];
|
|
||||||
pointAffineCtor.setAccessible(true);
|
|
||||||
|
|
||||||
yField = pointAffineClass.getDeclaredField("y");
|
|
||||||
yField.setAccessible(true);
|
|
||||||
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException | NoSuchFieldException | ClassNotFoundException e) {
|
|
||||||
throw new RuntimeException("Can't initialize BouncyCastle25519 shim", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int[] obtainYFromPublicKey(byte[] ed25519PublicKey) {
|
|
||||||
try {
|
|
||||||
Object pA = pointAffineCtor.newInstance();
|
|
||||||
|
|
||||||
Boolean result = (Boolean) decodePointVarMethod.invoke(null, ed25519PublicKey, 0, true, pA);
|
|
||||||
if (result == null || !result)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (int[]) yField.get(pA);
|
|
||||||
} catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
|
||||||
throw new RuntimeException("Can't reflect into BouncyCastle", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] toX25519PublicKey(byte[] ed25519PublicKey) {
|
|
||||||
int[] one = new int[X25519Field.SIZE];
|
|
||||||
X25519Field.one(one);
|
|
||||||
|
|
||||||
int[] y = obtainYFromPublicKey(ed25519PublicKey);
|
|
||||||
|
|
||||||
int[] oneMinusY = new int[X25519Field.SIZE];
|
|
||||||
X25519Field.sub(one, y, oneMinusY);
|
|
||||||
|
|
||||||
int[] onePlusY = new int[X25519Field.SIZE];
|
|
||||||
X25519Field.add(one, y, onePlusY);
|
|
||||||
|
|
||||||
int[] oneMinusYInverted = new int[X25519Field.SIZE];
|
|
||||||
X25519Field.inv(oneMinusY, oneMinusYInverted);
|
|
||||||
|
|
||||||
int[] u = new int[X25519Field.SIZE];
|
|
||||||
X25519Field.mul(onePlusY, oneMinusYInverted, u);
|
|
||||||
|
|
||||||
X25519Field.normalize(u);
|
|
||||||
|
|
||||||
byte[] x25519PublicKey = new byte[X25519.SCALAR_SIZE];
|
|
||||||
X25519Field.encode(u, x25519PublicKey, 0);
|
|
||||||
|
|
||||||
return x25519PublicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] toX25519PrivateKey(byte[] ed25519PrivateKey) {
|
|
||||||
Digest d = Ed25519.createPrehash();
|
|
||||||
byte[] h = new byte[d.getDigestSize()];
|
|
||||||
|
|
||||||
d.update(ed25519PrivateKey, 0, ed25519PrivateKey.length);
|
|
||||||
d.doFinal(h, 0);
|
|
||||||
|
|
||||||
byte[] s = new byte[X25519.SCALAR_SIZE];
|
|
||||||
|
|
||||||
System.arraycopy(h, 0, s, 0, X25519.SCALAR_SIZE);
|
|
||||||
s[0] &= 0xF8;
|
|
||||||
s[X25519.SCALAR_SIZE - 1] &= 0x7F;
|
|
||||||
s[X25519.SCALAR_SIZE - 1] |= 0x40;
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
1427
src/main/java/org/qortal/crypto/BouncyCastleEd25519.java
Normal file
1427
src/main/java/org/qortal/crypto/BouncyCastleEd25519.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -253,6 +253,10 @@ public abstract class Crypto {
|
|||||||
return false;
|
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) {
|
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
|
||||||
try {
|
try {
|
||||||
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
|
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) {
|
public static byte[] sign(Ed25519PrivateKeyParameters edPrivateKeyParams, byte[] message) {
|
||||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
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;
|
return signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] getSharedSecret(byte[] privateKey, byte[] publicKey) {
|
public static byte[] getSharedSecret(byte[] privateKey, byte[] publicKey) {
|
||||||
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(privateKey);
|
byte[] x25519PrivateKey = Qortal25519Extras.toX25519PrivateKey(privateKey);
|
||||||
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
|
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
|
||||||
|
|
||||||
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
|
byte[] x25519PublicKey = Qortal25519Extras.toX25519PublicKey(publicKey);
|
||||||
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
|
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
|
||||||
|
|
||||||
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
|
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
|
||||||
@ -281,5 +293,4 @@ public abstract class Crypto {
|
|||||||
|
|
||||||
return sharedSecret;
|
return sharedSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,44 @@
|
|||||||
package org.qortal.crypto;
|
package org.qortal.crypto;
|
||||||
|
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
public class MemoryPoW {
|
public class MemoryPoW {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a MemoryPoW nonce
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* @param workBufferLength
|
||||||
|
* @param difficulty
|
||||||
|
* @return
|
||||||
|
* @throws TimeoutException
|
||||||
|
*/
|
||||||
public static Integer compute2(byte[] data, int workBufferLength, long difficulty) {
|
public static Integer compute2(byte[] data, int workBufferLength, long difficulty) {
|
||||||
|
try {
|
||||||
|
return MemoryPoW.compute2(data, workBufferLength, difficulty, null);
|
||||||
|
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
// This won't happen, because above timeout is null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a MemoryPoW nonce, with optional timeout
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* @param workBufferLength
|
||||||
|
* @param difficulty
|
||||||
|
* @param timeout maximum number of milliseconds to compute for before giving up,<br>or null if no timeout
|
||||||
|
* @return
|
||||||
|
* @throws TimeoutException
|
||||||
|
*/
|
||||||
|
public static Integer compute2(byte[] data, int workBufferLength, long difficulty, Long timeout) throws TimeoutException {
|
||||||
|
long startTime = NTP.getTime();
|
||||||
|
|
||||||
// Hash data with SHA256
|
// Hash data with SHA256
|
||||||
byte[] hash = Crypto.digest(data);
|
byte[] hash = Crypto.digest(data);
|
||||||
|
|
||||||
@ -33,6 +67,13 @@ public class MemoryPoW {
|
|||||||
if (Thread.currentThread().isInterrupted())
|
if (Thread.currentThread().isInterrupted())
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
|
if (timeout != null) {
|
||||||
|
long now = NTP.getTime();
|
||||||
|
if (now > startTime + timeout) {
|
||||||
|
throw new TimeoutException("Timeout reached");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
seed *= seedMultiplier; // per nonce
|
seed *= seedMultiplier; // per nonce
|
||||||
|
|
||||||
state[0] = longHash[0] ^ seed;
|
state[0] = longHash[0] ^ seed;
|
||||||
@ -58,6 +99,10 @@ public class MemoryPoW {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
|
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
|
||||||
|
return verify2(data, null, workBufferLength, difficulty, nonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verify2(byte[] data, long[] workBuffer, int workBufferLength, long difficulty, int nonce) {
|
||||||
// Hash data with SHA256
|
// Hash data with SHA256
|
||||||
byte[] hash = Crypto.digest(data);
|
byte[] hash = Crypto.digest(data);
|
||||||
|
|
||||||
@ -70,7 +115,10 @@ public class MemoryPoW {
|
|||||||
byteBuffer = null;
|
byteBuffer = null;
|
||||||
|
|
||||||
int longBufferLength = workBufferLength / 8;
|
int longBufferLength = workBufferLength / 8;
|
||||||
long[] workBuffer = new long[longBufferLength];
|
|
||||||
|
if (workBuffer == null)
|
||||||
|
workBuffer = new long[longBufferLength];
|
||||||
|
|
||||||
long[] state = new long[4];
|
long[] state = new long[4];
|
||||||
|
|
||||||
long seed = 8682522807148012L;
|
long seed = 8682522807148012L;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ public class AccountData {
|
|||||||
protected int level;
|
protected int level;
|
||||||
protected int blocksMinted;
|
protected int blocksMinted;
|
||||||
protected int blocksMintedAdjustment;
|
protected int blocksMintedAdjustment;
|
||||||
|
protected int blocksMintedPenalty;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ public class AccountData {
|
|||||||
protected AccountData() {
|
protected AccountData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment) {
|
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment, int blocksMintedPenalty) {
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.reference = reference;
|
this.reference = reference;
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
@ -34,10 +35,11 @@ public class AccountData {
|
|||||||
this.level = level;
|
this.level = level;
|
||||||
this.blocksMinted = blocksMinted;
|
this.blocksMinted = blocksMinted;
|
||||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||||
|
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountData(String address) {
|
public AccountData(String address) {
|
||||||
this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0);
|
this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters/Setters
|
// Getters/Setters
|
||||||
@ -102,6 +104,14 @@ public class AccountData {
|
|||||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getBlocksMintedPenalty() {
|
||||||
|
return this.blocksMintedPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlocksMintedPenalty(int blocksMintedPenalty) {
|
||||||
|
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
// Comparison
|
// Comparison
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
package org.qortal.data.account;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
|
||||||
|
// All properties to be converted to JSON via JAXB
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class AccountPenaltyData {
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
private String address;
|
||||||
|
private int blocksMintedPenalty;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
// necessary for JAXB
|
||||||
|
protected AccountPenaltyData() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountPenaltyData(String address, int blocksMintedPenalty) {
|
||||||
|
this.address = address;
|
||||||
|
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters/Setters
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return this.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBlocksMintedPenalty() {
|
||||||
|
return this.blocksMintedPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s has penalty %d", this.address, this.blocksMintedPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object b) {
|
||||||
|
if (!(b instanceof AccountPenaltyData))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return this.getAddress().equals(((AccountPenaltyData) b).getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return address.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,19 +15,24 @@ public class ArbitraryResourceMetadata {
|
|||||||
private List<String> tags;
|
private List<String> tags;
|
||||||
private Category category;
|
private Category category;
|
||||||
private String categoryName;
|
private String categoryName;
|
||||||
|
private List<String> files;
|
||||||
|
|
||||||
public ArbitraryResourceMetadata() {
|
public ArbitraryResourceMetadata() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category) {
|
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category, List<String> files) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
this.category = category;
|
this.category = category;
|
||||||
this.categoryName = category.getName();
|
this.files = files;
|
||||||
|
|
||||||
|
if (category != null) {
|
||||||
|
this.categoryName = category.getName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata) {
|
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata, boolean includeFileList) {
|
||||||
if (transactionMetadata == null) {
|
if (transactionMetadata == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -36,10 +41,20 @@ public class ArbitraryResourceMetadata {
|
|||||||
List<String> tags = transactionMetadata.getTags();
|
List<String> tags = transactionMetadata.getTags();
|
||||||
Category category = transactionMetadata.getCategory();
|
Category category = transactionMetadata.getCategory();
|
||||||
|
|
||||||
if (title == null && description == null && tags == null && category == null) {
|
// We don't always want to include the file list as it can be too verbose
|
||||||
|
List<String> files = null;
|
||||||
|
if (includeFileList) {
|
||||||
|
files = transactionMetadata.getFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title == null && description == null && tags == null && category == null && files == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ArbitraryResourceMetadata(title, description, tags, category);
|
return new ArbitraryResourceMetadata(title, description, tags, category, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getFiles() {
|
||||||
|
return this.files;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ public class ArbitraryResourceStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Status status;
|
||||||
private String id;
|
private String id;
|
||||||
private String title;
|
private String title;
|
||||||
private String description;
|
private String description;
|
||||||
@ -37,6 +38,7 @@ public class ArbitraryResourceStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ArbitraryResourceStatus(Status status, Integer localChunkCount, Integer totalChunkCount) {
|
public ArbitraryResourceStatus(Status status, Integer localChunkCount, Integer totalChunkCount) {
|
||||||
|
this.status = status;
|
||||||
this.id = status.toString();
|
this.id = status.toString();
|
||||||
this.title = status.title;
|
this.title = status.title;
|
||||||
this.description = status.description;
|
this.description = status.description;
|
||||||
@ -47,4 +49,20 @@ public class ArbitraryResourceStatus {
|
|||||||
public ArbitraryResourceStatus(Status status) {
|
public ArbitraryResourceStatus(Status status) {
|
||||||
this(status, null, null);
|
this(status, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Status getStatus() {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getLocalChunkCount() {
|
||||||
|
return this.localChunkCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTotalChunkCount() {
|
||||||
|
return this.totalChunkCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,6 +211,14 @@ public class BlockData implements Serializable {
|
|||||||
this.onlineAccountsSignatures = onlineAccountsSignatures;
|
this.onlineAccountsSignatures = onlineAccountsSignatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getOnlineAccountsSignaturesCount() {
|
||||||
|
if (this.onlineAccountsSignatures != null && this.onlineAccountsSignatures.length > 0) {
|
||||||
|
// Blocks use a single online accounts signature, so there is no need for this to be dynamic
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTrimmed() {
|
public boolean isTrimmed() {
|
||||||
long onlineAccountSignaturesTrimmedTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
|
long onlineAccountSignaturesTrimmedTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
|
||||||
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||||
|
@ -11,11 +11,12 @@ public class BlockSummaryData {
|
|||||||
private int height;
|
private int height;
|
||||||
private byte[] signature;
|
private byte[] signature;
|
||||||
private byte[] minterPublicKey;
|
private byte[] minterPublicKey;
|
||||||
private int onlineAccountsCount;
|
|
||||||
|
|
||||||
// Optional, set during construction
|
// Optional, set during construction
|
||||||
|
private Integer onlineAccountsCount;
|
||||||
private Long timestamp;
|
private Long timestamp;
|
||||||
private Integer transactionCount;
|
private Integer transactionCount;
|
||||||
|
private byte[] reference;
|
||||||
|
|
||||||
// Optional, set after construction
|
// Optional, set after construction
|
||||||
private Integer minterLevel;
|
private Integer minterLevel;
|
||||||
@ -25,6 +26,15 @@ public class BlockSummaryData {
|
|||||||
protected BlockSummaryData() {
|
protected BlockSummaryData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Constructor typically populated with fields from HeightV2Message */
|
||||||
|
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, long timestamp) {
|
||||||
|
this.height = height;
|
||||||
|
this.signature = signature;
|
||||||
|
this.minterPublicKey = minterPublicKey;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Constructor typically populated with fields from BlockSummariesMessage */
|
||||||
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount) {
|
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount) {
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.signature = signature;
|
this.signature = signature;
|
||||||
@ -32,13 +42,16 @@ public class BlockSummaryData {
|
|||||||
this.onlineAccountsCount = onlineAccountsCount;
|
this.onlineAccountsCount = onlineAccountsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount, long timestamp, int transactionCount) {
|
/** Constructor typically populated with fields from BlockSummariesV2Message */
|
||||||
|
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, Integer onlineAccountsCount,
|
||||||
|
Long timestamp, Integer transactionCount, byte[] reference) {
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.signature = signature;
|
this.signature = signature;
|
||||||
this.minterPublicKey = minterPublicKey;
|
this.minterPublicKey = minterPublicKey;
|
||||||
this.onlineAccountsCount = onlineAccountsCount;
|
this.onlineAccountsCount = onlineAccountsCount;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.transactionCount = transactionCount;
|
this.transactionCount = transactionCount;
|
||||||
|
this.reference = reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BlockSummaryData(BlockData blockData) {
|
public BlockSummaryData(BlockData blockData) {
|
||||||
@ -49,6 +62,7 @@ public class BlockSummaryData {
|
|||||||
|
|
||||||
this.timestamp = blockData.getTimestamp();
|
this.timestamp = blockData.getTimestamp();
|
||||||
this.transactionCount = blockData.getTransactionCount();
|
this.transactionCount = blockData.getTransactionCount();
|
||||||
|
this.reference = blockData.getReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters / setters
|
// Getters / setters
|
||||||
@ -65,7 +79,7 @@ public class BlockSummaryData {
|
|||||||
return this.minterPublicKey;
|
return this.minterPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getOnlineAccountsCount() {
|
public Integer getOnlineAccountsCount() {
|
||||||
return this.onlineAccountsCount;
|
return this.onlineAccountsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +91,10 @@ public class BlockSummaryData {
|
|||||||
return this.transactionCount;
|
return this.transactionCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getReference() {
|
||||||
|
return this.reference;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getMinterLevel() {
|
public Integer getMinterLevel() {
|
||||||
return this.minterLevel;
|
return this.minterLevel;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package org.qortal.data.block;
|
package org.qortal.data.block;
|
||||||
|
|
||||||
import org.qortal.data.network.PeerChainTipData;
|
|
||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@ -14,14 +12,14 @@ public class CommonBlockData {
|
|||||||
private BlockSummaryData commonBlockSummary = null;
|
private BlockSummaryData commonBlockSummary = null;
|
||||||
private List<BlockSummaryData> blockSummariesAfterCommonBlock = null;
|
private List<BlockSummaryData> blockSummariesAfterCommonBlock = null;
|
||||||
private BigInteger chainWeight = null;
|
private BigInteger chainWeight = null;
|
||||||
private PeerChainTipData chainTipData = null;
|
private BlockSummaryData chainTipData = null;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
protected CommonBlockData() {
|
protected CommonBlockData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommonBlockData(BlockSummaryData commonBlockSummary, PeerChainTipData chainTipData) {
|
public CommonBlockData(BlockSummaryData commonBlockSummary, BlockSummaryData chainTipData) {
|
||||||
this.commonBlockSummary = commonBlockSummary;
|
this.commonBlockSummary = commonBlockSummary;
|
||||||
this.chainTipData = chainTipData;
|
this.chainTipData = chainTipData;
|
||||||
}
|
}
|
||||||
@ -49,7 +47,7 @@ public class CommonBlockData {
|
|||||||
this.chainWeight = chainWeight;
|
this.chainWeight = chainWeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PeerChainTipData getChainTipData() {
|
public BlockSummaryData getChainTipData() {
|
||||||
return this.chainTipData;
|
return this.chainTipData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ public class ChatMessage {
|
|||||||
|
|
||||||
private String recipientName;
|
private String recipientName;
|
||||||
|
|
||||||
|
private byte[] chatReference;
|
||||||
|
|
||||||
private byte[] data;
|
private byte[] data;
|
||||||
|
|
||||||
private boolean isText;
|
private boolean isText;
|
||||||
@ -42,8 +44,8 @@ public class ChatMessage {
|
|||||||
|
|
||||||
// For repository use
|
// For repository use
|
||||||
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
|
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
|
||||||
String senderName, String recipient, String recipientName, byte[] data, boolean isText,
|
String senderName, String recipient, String recipientName, byte[] chatReference, byte[] data,
|
||||||
boolean isEncrypted, byte[] signature) {
|
boolean isText, boolean isEncrypted, byte[] signature) {
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.txGroupId = txGroupId;
|
this.txGroupId = txGroupId;
|
||||||
this.reference = reference;
|
this.reference = reference;
|
||||||
@ -52,6 +54,7 @@ public class ChatMessage {
|
|||||||
this.senderName = senderName;
|
this.senderName = senderName;
|
||||||
this.recipient = recipient;
|
this.recipient = recipient;
|
||||||
this.recipientName = recipientName;
|
this.recipientName = recipientName;
|
||||||
|
this.chatReference = chatReference;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.isText = isText;
|
this.isText = isText;
|
||||||
this.isEncrypted = isEncrypted;
|
this.isEncrypted = isEncrypted;
|
||||||
@ -90,6 +93,10 @@ public class ChatMessage {
|
|||||||
return this.recipientName;
|
return this.recipientName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getChatReference() {
|
||||||
|
return this.chatReference;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getData() {
|
public byte[] getData() {
|
||||||
return this.data;
|
return this.data;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import java.util.Arrays;
|
|||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
import javax.xml.bind.annotation.XmlElement;
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
|
import javax.xml.bind.annotation.XmlTransient;
|
||||||
|
|
||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
|
|
||||||
@ -15,6 +16,10 @@ public class OnlineAccountData {
|
|||||||
protected long timestamp;
|
protected long timestamp;
|
||||||
protected byte[] signature;
|
protected byte[] signature;
|
||||||
protected byte[] publicKey;
|
protected byte[] publicKey;
|
||||||
|
protected Integer nonce;
|
||||||
|
|
||||||
|
@XmlTransient
|
||||||
|
private int hash;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
@ -22,10 +27,15 @@ public class OnlineAccountData {
|
|||||||
protected OnlineAccountData() {
|
protected OnlineAccountData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
|
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey, Integer nonce) {
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.signature = signature;
|
this.signature = signature;
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
|
this.nonce = nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
|
||||||
|
this(timestamp, signature, publicKey, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getTimestamp() {
|
public long getTimestamp() {
|
||||||
@ -40,6 +50,10 @@ public class OnlineAccountData {
|
|||||||
return this.publicKey;
|
return this.publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getNonce() {
|
||||||
|
return this.nonce;
|
||||||
|
}
|
||||||
|
|
||||||
// For JAXB
|
// For JAXB
|
||||||
@XmlElement(name = "address")
|
@XmlElement(name = "address")
|
||||||
protected String getAddress() {
|
protected String getAddress() {
|
||||||
@ -62,20 +76,23 @@ public class OnlineAccountData {
|
|||||||
if (otherOnlineAccountData.timestamp != this.timestamp)
|
if (otherOnlineAccountData.timestamp != this.timestamp)
|
||||||
return false;
|
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))
|
if (!Arrays.equals(otherOnlineAccountData.publicKey, this.publicKey))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// We don't compare signature because it's not our remit to verify and newer aggregate signatures use random nonces
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
// Pretty lazy implementation
|
int h = this.hash;
|
||||||
return (int) this.timestamp;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
package org.qortal.data.network;
|
|
||||||
|
|
||||||
public class PeerChainTipData {
|
|
||||||
|
|
||||||
/** Latest block height as reported by peer. */
|
|
||||||
private Integer lastHeight;
|
|
||||||
/** Latest block signature as reported by peer. */
|
|
||||||
private byte[] lastBlockSignature;
|
|
||||||
/** Latest block timestamp as reported by peer. */
|
|
||||||
private Long lastBlockTimestamp;
|
|
||||||
/** Latest block minter public key as reported by peer. */
|
|
||||||
private byte[] lastBlockMinter;
|
|
||||||
|
|
||||||
public PeerChainTipData(Integer lastHeight, byte[] lastBlockSignature, Long lastBlockTimestamp, byte[] lastBlockMinter) {
|
|
||||||
this.lastHeight = lastHeight;
|
|
||||||
this.lastBlockSignature = lastBlockSignature;
|
|
||||||
this.lastBlockTimestamp = lastBlockTimestamp;
|
|
||||||
this.lastBlockMinter = lastBlockMinter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getLastHeight() {
|
|
||||||
return this.lastHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getLastBlockSignature() {
|
|
||||||
return this.lastBlockSignature;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getLastBlockTimestamp() {
|
|
||||||
return this.lastBlockTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getLastBlockMinter() {
|
|
||||||
return this.lastBlockMinter;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user