Merge branch 'master' into german

This commit is contained in:
CalDescent 2023-01-28 20:22:02 +00:00 committed by GitHub
commit 42f2d015b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
229 changed files with 42904 additions and 3630 deletions

View File

@ -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.

View File

@ -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(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(appDir + &quot;log4j2.properties&quot;, appDir + &quot;log4j2-orig.properties&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite %AppDir%\log4j2.properties to update logfile storage path&#13;&#10;var fin = fso.OpenTextFile(appDir + &quot;log4j2-orig.properties&quot;, ForReading, false); // no create&#13;&#10;var fout = fso.OpenTextFile(appDir + &quot;log4j2.properties&quot;, ForWriting, true); // can create&#13;&#10;&#13;&#10;// Copy lines with rewriting where necessary&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;var line = fin.ReadLine();&#13;&#10;&#13;&#10;&#9;var start = line.indexOf(&quot;property.dirname&quot;);&#13;&#10;&#9;if (start &gt; 0) {&#13;&#10;&#9;&#9;// line: # property.dirname = ...appdata...&#13;&#10;&#9;&#9;// uncomment/replace this line for Windows&#13;&#10;&#9;&#9;fout.WriteLine( &quot;property.dirname = &quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) );&#13;&#10;&#9;} else {&#13;&#10;&#9;&#9;// not found - output verbatim&#13;&#10;&#9;&#9;fout.WriteLine( line );&#13;&#10;&#9;}&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_4"/> <ROW Action="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(appDir + &quot;log4j2.properties&quot;, appDir + &quot;log4j2-orig.properties&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite %AppDir%\log4j2.properties to update logfile storage path&#13;&#10;var fin = fso.OpenTextFile(appDir + &quot;log4j2-orig.properties&quot;, ForReading, false); // no create&#13;&#10;var fout = fso.OpenTextFile(appDir + &quot;log4j2.properties&quot;, ForWriting, true); // can create&#13;&#10;&#13;&#10;// Copy lines with rewriting where necessary&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;var line = fin.ReadLine();&#13;&#10;&#13;&#10;&#9;var start = line.indexOf(&quot;property.dirname&quot;);&#13;&#10;&#9;if (start &gt; 0) {&#13;&#10;&#9;&#9;// line: # property.dirname = ...appdata...&#13;&#10;&#9;&#9;// uncomment/replace this line for Windows&#13;&#10;&#9;&#9;fout.WriteLine( &quot;property.dirname = &quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) );&#13;&#10;&#9;} else {&#13;&#10;&#9;&#9;// not found - output verbatim&#13;&#10;&#9;&#9;fout.WriteLine( line );&#13;&#10;&#9;}&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_4"/>
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder&#13;&#10;var fts = fso.OpenTextFile(appDir + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;fts.WriteLine( &quot;{&quot; );&#13;&#10;// We need to escape Windows path backslashes to keep JSON valid&#13;&#10;fts.WriteLine( &quot; \&quot;userPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;\&quot;&quot; );&#13;&#10;fts.WriteLine( &quot;}&quot; );&#13;&#10;&#13;&#10;fts.Close();&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(dataFolder + &quot;settings.json&quot;, dataFolder + &quot;settings-orig.json&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite settings.json to update repository path&#13;&#10;var fin = fso.OpenTextFile(dataFolder + &quot;settings-orig.json&quot;, ForReading, false);&#13;&#10;var fout = fso.OpenTextFile(dataFolder + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;// First line should contain opening brace&#13;&#10;fout.WriteLine( fin.ReadLine() );&#13;&#10;&#13;&#10;// Append our entries&#13;&#10;fout.WriteLine( &quot; \&quot;repositoryPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;db\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;dataPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;data\&quot;,&quot; );&#13;&#10;&#13;&#10;// copy rest of settings&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;fout.WriteLine( fin.ReadLine() );&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_3"/> <ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder&#13;&#10;var fts = fso.OpenTextFile(appDir + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;fts.WriteLine( &quot;{&quot; );&#13;&#10;// We need to escape Windows path backslashes to keep JSON valid&#13;&#10;fts.WriteLine( &quot; \&quot;userPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;\&quot;&quot; );&#13;&#10;fts.WriteLine( &quot;}&quot; );&#13;&#10;&#13;&#10;fts.Close();&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(dataFolder + &quot;settings.json&quot;, dataFolder + &quot;settings-orig.json&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite settings.json to update repository path&#13;&#10;var fin = fso.OpenTextFile(dataFolder + &quot;settings-orig.json&quot;, ForReading, false);&#13;&#10;var fout = fso.OpenTextFile(dataFolder + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;// First line should contain opening brace&#13;&#10;fout.WriteLine( fin.ReadLine() );&#13;&#10;&#13;&#10;// Append our entries&#13;&#10;fout.WriteLine( &quot; \&quot;repositoryPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;db\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;dataPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;data\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;walletsPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;wallets\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;listsPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;lists\&quot;,&quot; );&#13;&#10;&#13;&#10;// copy rest of settings&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;fout.WriteLine( fin.ReadLine() );&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" 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"/>

Binary file not shown.

View 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>

View File

@ -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
View File

@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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;
}
}

View File

@ -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()));
} }
} }

View File

@ -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());

View File

@ -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);
} }

View 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;
}
}

View 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);
}
}

View File

@ -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);
} }
} }

View File

@ -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();

View File

@ -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() {
}
}

View File

@ -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(

View File

@ -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")

View File

@ -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;
} }

View File

@ -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;

View File

@ -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(

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
} }
} }

View File

@ -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);

View File

@ -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());
}
}
}

View File

@ -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);

View File

@ -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());
} }
} }

View File

@ -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 {

View File

@ -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,

View File

@ -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");

View File

@ -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());
}
} }
} }

View File

@ -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)) {

View File

@ -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;

View File

@ -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);
}
} }
} }

View File

@ -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;

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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)) {

View File

@ -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;

View File

@ -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) {

View File

@ -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);

View 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)));
}
}

View File

@ -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 {

View File

@ -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);

View File

@ -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

View File

@ -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...";
}
}

View File

@ -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: {

View File

@ -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 {

View File

@ -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");

View File

@ -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);
} }
/** /**

View File

@ -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 {

View File

@ -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();

View File

@ -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();

View File

@ -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");

View File

@ -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;
} }

View File

@ -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;
}
}

View File

@ -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));

View File

@ -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));
} }
/** /**

View File

@ -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();
} }
} }

View File

@ -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;

View File

@ -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");
} }

View File

@ -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;

View File

@ -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"

View File

@ -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;

View File

@ -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;

View File

@ -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();

View 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);
// }
}

View File

@ -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;

View File

@ -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;
}
}

View 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);
}
}

View File

@ -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;
} }

View 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);
}
}

View 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);
}
}
}

View 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];
}
}

View File

@ -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;

View File

@ -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;
} }

View File

@ -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())

View File

@ -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);
} }
} }

View File

@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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;
} }
} }

View File

@ -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;

View 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);
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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;
} }
} }

View File

@ -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;
}
} }

View File

@ -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();

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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;
} }
} }

View File

@ -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