forked from Qortal/qortal
Compare commits
208 Commits
shares-by-
...
v3.8.4
Author | SHA1 | Date | |
---|---|---|---|
|
9d81ea7744 | ||
|
688acd466c | ||
|
81cf46f5dd | ||
|
4c52d6f0fc | ||
|
c03f271825 | ||
|
dfe3754afc | ||
|
30105199a2 | ||
|
e91e612b55 | ||
|
2a55eba1f7 | ||
|
39e59cbcf8 | ||
|
016191bdb0 | ||
|
0596a07c7d | ||
|
c62c59b445 | ||
|
f78101e9cc | ||
|
476fdcb31d | ||
|
02d5043ef7 | ||
|
0ad9e2f65b | ||
|
4dc0033a5a | ||
|
745cfe8ea1 | ||
|
6284a4691c | ||
|
41f88be55e | ||
|
ba95f8376f | ||
|
8e97c05b56 | ||
|
eb569304ba | ||
|
b0486f44bb | ||
|
cecf28ab7b | ||
|
98b92a5bf1 | ||
|
6b45901c47 | ||
|
166f9bd079 | ||
|
2f8f896077 | ||
|
9a77aff0a6 | ||
|
c6d65a88dc | ||
|
4aea29a91b | ||
|
0e81665a36 | ||
|
2a4ac1ed24 | ||
|
bb74b2d4f6 | ||
|
758a02d71a | ||
|
7ae142fa64 | ||
|
a75ed0e634 | ||
|
e40dc4af59 | ||
|
e678ea22e0 | ||
|
cf3195cb83 | ||
|
80048208d1 | ||
|
08de1fb4ec | ||
|
99d5bf9103 | ||
|
1dc7f056f9 | ||
|
cdeb2052b0 | ||
|
5c9109aca9 | ||
|
ccc1976d00 | ||
|
12fb6cd0ad | ||
|
6f95e7c1c8 | ||
|
a69618133e | ||
|
51ad0a5b48 | ||
|
45a6f495d2 | ||
|
4d9964c080 | ||
|
9afc31a20d | ||
|
d435e4047b | ||
|
c108afa27c | ||
|
eea42b56ee | ||
|
f4d20e42f3 | ||
|
f14cc374c6 | ||
|
99ba4caf75 | ||
|
ae991dda4d | ||
|
2b6ae57a27 | ||
|
5ff7b3df6d | ||
|
76686eca21 | ||
|
3965f24ab5 | ||
|
a75fd14e45 | ||
|
41cdf665ed | ||
|
6ea3c0e6f7 | ||
|
5f0263c078 | ||
|
58e5d325ff | ||
|
7003a8274b | ||
|
ab687af4bb | ||
|
f50c0c87dd | ||
|
9c3a4d6e37 | ||
|
1c8a6ce204 | ||
|
68a0923582 | ||
|
617c801cbd | ||
|
b0c9ce7482 | ||
|
4e829a2d05 | ||
|
a7402adfa5 | ||
|
9255df46cf | ||
|
db22445948 | ||
|
818e037e75 | ||
|
9c68f1038a | ||
|
10ae383bb6 | ||
|
aead9cfcbf | ||
|
985c195e9e | ||
|
0628847d14 | ||
|
4043ae1928 | ||
|
fa80c83864 | ||
|
f739d8f5c6 | ||
|
166425bee9 | ||
|
59a804c560 | ||
|
b64c053531 | ||
|
30cd56165a | ||
|
510328db47 | ||
|
9d74f0eec0 | ||
|
09014d07e0 | ||
|
f83d4bac7b | ||
|
b3273ff01a | ||
|
1dd039fb2d | ||
|
1d5497e484 | ||
|
b37aa749c6 | ||
|
e45ad37eb5 | ||
|
72985b1fc6 | ||
|
6f27d3798c | ||
|
23a5c5f9b4 | ||
|
a4759a0ef4 | ||
|
910191b074 | ||
|
57125a91cf | ||
|
3c565638c1 | ||
|
c2d02aead9 | ||
|
0d9aafaf4e | ||
|
3844358380 | ||
|
b4125d2bf1 | ||
|
5c223179ed | ||
|
f3cb57417a | ||
|
7c7f071eba | ||
|
7c15d88cbc | ||
|
d4aaba2293 | ||
|
10d3176e70 | ||
|
36fcd6792a | ||
|
cb1eee8ff5 | ||
|
2d58118d7c | ||
|
e6bb0b81cf | ||
|
77d60fc33f | ||
|
504f38b42a | ||
|
3a18599d85 | ||
|
0088ba8485 | ||
|
8cedf618f4 | ||
|
fdd95eac56 | ||
|
10b0f0a054 | ||
|
1233ba6703 | ||
|
c35c7180d4 | ||
|
7080b55aac | ||
|
3890fa8490 | ||
|
a9721bab3d | ||
|
1bb8f1b6d2 | ||
|
765416db71 | ||
|
5989473c8a | ||
|
aa9da45c01 | ||
|
4681218416 | ||
|
5c746f0bd9 | ||
|
309f27a6b8 | ||
|
d2ebb215e6 | ||
|
7a60f713ea | ||
|
e80dd31fb4 | ||
|
94cdc10151 | ||
|
863a5eff97 | ||
|
5b81b30974 | ||
|
174a779e4c | ||
|
c7cf33ef78 | ||
|
ea4f4d949b | ||
|
6d9e6e8d4c | ||
|
99858f3781 | ||
|
84a16157d1 | ||
|
49d83650f4 | ||
|
951c85faf1 | ||
|
84d42b93e1 | ||
|
b99b1f5d57 | ||
|
952c51ab25 | ||
|
64ef8ab863 | ||
|
93fd80e289 | ||
|
5581b83c57 | ||
|
5017072f6c | ||
|
02ac6dd8c1 | ||
|
858269f6cb | ||
|
791a9b78ec | ||
|
aff49e6bdf | ||
|
2d29fdca00 | ||
|
063ef8507b | ||
|
f042b5ca5f | ||
|
a10e669554 | ||
|
501f66ab00 | ||
|
6003ed3ff7 | ||
|
03e3619817 | ||
|
0e42e7b05a | ||
|
d4fbc1687b | ||
|
8ffdc9b369 | ||
|
c883dd44c8 | ||
|
667530e202 | ||
|
5807d6e0dc | ||
|
ba4eeed358 | ||
|
82edc4d9f3 | ||
|
2a0d5746e6 | ||
|
23423102e7 | ||
|
8879ec5bb4 | ||
|
8cca6db316 | ||
|
effe1ac44d | ||
|
ad4308afdf | ||
|
6cfd85bdce | ||
|
73396490ba | ||
|
b9bf945fd8 | ||
|
85a27c14b8 | ||
|
46c40ca9ca | ||
|
fbcc870d36 | ||
|
020e59743b | ||
|
0904de3f71 | ||
|
fe2c63e8e4 | ||
|
a3febdf00e | ||
|
4ca174fa0b | ||
|
294582f136 | ||
|
215800fb67 | ||
|
b05d428b2e | ||
|
d2adadb600 | ||
|
8e8c0b3fc5 |
44
TestNets.md
44
TestNets.md
@@ -52,14 +52,13 @@
|
||||
|
||||
## Single-node testnet
|
||||
|
||||
A single-node testnet is possible with code modifications, for basic testing, or to more easily start a new testnet.
|
||||
To do so, follow these steps:
|
||||
- Comment out the `if (mintedLastBlock) { }` conditional in BlockMinter.java
|
||||
- Comment out the `minBlockchainPeers` validation in Settings.validate()
|
||||
- Set `minBlockchainPeers` to 0 in settings.json
|
||||
- Set `Synchronizer.RECOVERY_MODE_TIMEOUT` 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
|
||||
A single-node testnet is possible with an additional settings, or to more easily start a new testnet.
|
||||
Just add this setting:
|
||||
```
|
||||
"singleNodeTestnet": true
|
||||
```
|
||||
This will automatically allow multiple consecutive blocks to be minted, as well as setting minBlockchainPeers to 0.
|
||||
Remember to put these values back after introducing other nodes
|
||||
|
||||
## 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 ......`
|
||||
- `peer-heights`, but use `-t` option, or `BASE_URL` shell variable as above
|
||||
|
||||
## Example settings-test.json
|
||||
```
|
||||
{
|
||||
"isTestNet": true,
|
||||
"bitcoinNet": "TEST3",
|
||||
"repositoryPath": "db-testnet",
|
||||
"blockchainConfig": "testchain.json",
|
||||
"minBlockchainPeers": 1,
|
||||
"apiDocumentationEnabled": true,
|
||||
"apiRestricted": false,
|
||||
"bootstrap": false,
|
||||
"maxPeerConnectionTime": 999999999,
|
||||
"localAuthBypassEnabled": true,
|
||||
"singleNodeTestnet": true,
|
||||
"recoveryModeTimeout": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Quick start
|
||||
Here are some steps to quickly get a single node testnet up and running with a generic minting account:
|
||||
1. Start with template `settings-test.json`, and create a `testchain.json` based on mainnet's blockchain.json (or obtain one from Qortal developers). These should be in the same directory as the jar.
|
||||
2. Make sure feature triggers and other timestamp/height activations are correctly set. Generally these would be `0` so that they are enabled from the start.
|
||||
3. Set a recent genesis `timestamp` in testchain.json, and add this reward share entry:
|
||||
`{ "type": "REWARD_SHARE", "minterPublicKey": "DwcUnhxjamqppgfXCLgbYRx8H9XFPUc2qYRy3CEvQWEw", "recipient": "QbTDMss7NtRxxQaSqBZtSLSNdSYgvGaqFf", "rewardSharePublicKey": "CRvQXxFfUMfr4q3o1PcUZPA4aPCiubBsXkk47GzRo754", "sharePercent": 0 },`
|
||||
4. Start the node, passing in settings-test.json, e.g: `java -jar qortal.jar settings-test.json`
|
||||
5. Once started, add the corresponding minting key to the node:
|
||||
`curl -X POST "http://localhost:62391/admin/mintingaccounts" -d "F48mYJycFgRdqtc58kiovwbcJgVukjzRE4qRRtRsK9ix"`
|
||||
6. Alternatively you can use your own minting account instead of the generic one above.
|
||||
7. After a short while, blocks should be minted from the genesis timestamp until the current time.
|
@@ -17,10 +17,10 @@
|
||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||
<ROW Property="NTP_GOOD" Value="false"/>
|
||||
<ROW Property="ProductCode" Value="1033:{E5597539-098E-4BA6-99DF-4D22018BC0D3} 1049:{2B5E55A2-142A-4BED-B3B9-5657162282B7} 2052:{6F19171F-4743-4127-B191-AAFA3FA885D2} 2057:{A1B3108D-EC5D-47A1-AEE4-DBD956E682FB} " Type="16"/>
|
||||
<ROW Property="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="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="3.4.3" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="3.8.3" Type="32"/>
|
||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
@@ -212,7 +212,7 @@
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{F17029E8-CCC4-456D-B4AC-1854C81C46B6}" 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="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"/>
|
||||
@@ -1173,7 +1173,7 @@
|
||||
<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="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Make copy fso.CopyFile(appDir + "log4j2.properties", appDir + "log4j2-orig.properties", true); // overwrite // Rewrite %AppDir%\log4j2.properties to update logfile storage path var fin = fso.OpenTextFile(appDir + "log4j2-orig.properties", ForReading, false); // no create var fout = fso.OpenTextFile(appDir + "log4j2.properties", ForWriting, true); // can create // Copy lines with rewriting where necessary while( !fin.AtEndOfStream ) { 	var line = fin.ReadLine(); 	var start = line.indexOf("property.dirname"); 	if (start > 0) { 		// line: # property.dirname = ...appdata... 		// uncomment/replace this line for Windows 		fout.WriteLine( "property.dirname = " + dataFolder.split('\\').join('\\\\') ); 	} else { 		// not found - output verbatim 		fout.WriteLine( line ); 	} } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_4"/>
|
||||
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder var fts = fso.OpenTextFile(appDir + "settings.json", ForWriting, true); fts.WriteLine( "{" ); // We need to escape Windows path backslashes to keep JSON valid fts.WriteLine( " \"userPath\": \"" + dataFolder.split('\\').join('\\\\') + "\"" ); fts.WriteLine( "}" ); fts.Close(); // Make copy fso.CopyFile(dataFolder + "settings.json", dataFolder + "settings-orig.json", true); // overwrite // Rewrite settings.json to update repository path var fin = fso.OpenTextFile(dataFolder + "settings-orig.json", ForReading, false); var fout = fso.OpenTextFile(dataFolder + "settings.json", ForWriting, true); // First line should contain opening brace fout.WriteLine( fin.ReadLine() ); // Append our entries fout.WriteLine( " \"repositoryPath\": \"" + dataFolder.split('\\').join('\\\\') + "db\"," ); fout.WriteLine( " \"dataPath\": \"" + dataFolder.split('\\').join('\\\\') + "data\"," ); // copy rest of settings while( !fin.AtEndOfStream ) { 	fout.WriteLine( fin.ReadLine() ); } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_3"/>
|
||||
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder var fts = fso.OpenTextFile(appDir + "settings.json", ForWriting, true); fts.WriteLine( "{" ); // We need to escape Windows path backslashes to keep JSON valid fts.WriteLine( " \"userPath\": \"" + dataFolder.split('\\').join('\\\\') + "\"" ); fts.WriteLine( "}" ); fts.Close(); // Make copy fso.CopyFile(dataFolder + "settings.json", dataFolder + "settings-orig.json", true); // overwrite // Rewrite settings.json to update repository path var fin = fso.OpenTextFile(dataFolder + "settings-orig.json", ForReading, false); var fout = fso.OpenTextFile(dataFolder + "settings.json", ForWriting, true); // First line should contain opening brace fout.WriteLine( fin.ReadLine() ); // Append our entries fout.WriteLine( " \"repositoryPath\": \"" + dataFolder.split('\\').join('\\\\') + "db\"," ); fout.WriteLine( " \"dataPath\": \"" + dataFolder.split('\\').join('\\\\') + "data\"," ); fout.WriteLine( " \"walletsPath\": \"" + dataFolder.split('\\').join('\\\\') + "wallets\"," ); fout.WriteLine( " \"listsPath\": \"" + dataFolder.split('\\').join('\\\\') + "lists\"," ); // copy rest of settings while( !fin.AtEndOfStream ) { 	fout.WriteLine( fin.ReadLine() ); } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_3"/>
|
||||
<ROW Action="DetectRunningProcess" Type="1" Source="aicustact.dll" Target="DetectProcess" Options="3" AdditionalSeq="AI_DATA_SETTER_8"/>
|
||||
<ROW Action="DetectW32Time" Type="1" Source="aicustact.dll" Target="DetectService" Options="3" AdditionalSeq="AI_DATA_SETTER_11"/>
|
||||
<ROW Action="NTP_config" Type="3090" Source="ntpcfg.bat"/>
|
||||
|
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
@@ -3,14 +3,15 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>AT</artifactId>
|
||||
<versioning>
|
||||
<release>1.3.8</release>
|
||||
<release>1.4.0</release>
|
||||
<versions>
|
||||
<version>1.3.4</version>
|
||||
<version>1.3.5</version>
|
||||
<version>1.3.6</version>
|
||||
<version>1.3.7</version>
|
||||
<version>1.3.8</version>
|
||||
<version>1.4.0</version>
|
||||
</versions>
|
||||
<lastUpdated>20200925114415</lastUpdated>
|
||||
<lastUpdated>20221105114346</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
4
pom.xml
4
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<version>3.8.4</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
@@ -11,7 +11,7 @@
|
||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||
<bouncycastle.version>1.69</bouncycastle.version>
|
||||
<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-text.version>1.8</commons-text.version>
|
||||
<commons-io.version>2.6</commons-io.version>
|
||||
|
@@ -211,7 +211,8 @@ public class Account {
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
||||
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 false;
|
||||
@@ -222,6 +223,11 @@ public class Account {
|
||||
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.
|
||||
* <p>
|
||||
@@ -243,7 +249,7 @@ public class Account {
|
||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
|
||||
return true;
|
||||
|
||||
if (Account.isFounder(accountData.getFlags()))
|
||||
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
@@ -271,7 +277,7 @@ public class Account {
|
||||
/**
|
||||
* Returns 'effective' minting level, or zero if account does not exist/cannot mint.
|
||||
* <p>
|
||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
* For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
*
|
||||
* @return 0+
|
||||
* @throws DataException
|
||||
@@ -281,7 +287,8 @@ public class Account {
|
||||
if (accountData == null)
|
||||
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 accountData.getLevel();
|
||||
@@ -289,8 +296,6 @@ public class Account {
|
||||
|
||||
/**
|
||||
* 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 rewardSharePublicKey
|
||||
@@ -309,7 +314,7 @@ public class Account {
|
||||
/**
|
||||
* Returns 'effective' minting level, with a fix for the zero level.
|
||||
* <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 rewardSharePublicKey
|
||||
@@ -322,7 +327,7 @@ public class Account {
|
||||
if (rewardShareData == null)
|
||||
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;
|
||||
|
||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||
|
367
src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
Normal file
367
src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
Normal file
@@ -0,0 +1,367 @@
|
||||
package org.qortal.account;
|
||||
|
||||
import org.qortal.api.resource.TransactionsResource;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SelfSponsorshipAlgoV1 {
|
||||
|
||||
private final Repository repository;
|
||||
private final String address;
|
||||
private final AccountData accountData;
|
||||
private final long snapshotTimestamp;
|
||||
private final boolean override;
|
||||
|
||||
private int registeredNameCount = 0;
|
||||
private int suspiciousCount = 0;
|
||||
private int suspiciousPercent = 0;
|
||||
private int consolidationCount = 0;
|
||||
private int bulkIssuanceCount = 0;
|
||||
private int recentSponsorshipCount = 0;
|
||||
|
||||
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
private final Map<String, List<TransactionData>> paymentsByAddress = new HashMap<>();
|
||||
private final Set<String> sponsees = new LinkedHashSet<>();
|
||||
private Set<String> consolidatedAddresses = new LinkedHashSet<>();
|
||||
private final Set<String> zeroTransactionAddreses = new LinkedHashSet<>();
|
||||
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
|
||||
|
||||
public SelfSponsorshipAlgoV1(Repository repository, String address, long snapshotTimestamp, boolean override) throws DataException {
|
||||
this.repository = repository;
|
||||
this.address = address;
|
||||
this.accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||
this.snapshotTimestamp = snapshotTimestamp;
|
||||
this.override = override;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public Set<String> getPenaltyAddresses() {
|
||||
return this.penaltyAddresses;
|
||||
}
|
||||
|
||||
|
||||
public void run() throws DataException {
|
||||
if (this.accountData == null) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchSponsorshipRewardShares();
|
||||
if (this.sponsorshipRewardShares.isEmpty()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
this.findConsolidatedRewards();
|
||||
this.findBulkIssuance();
|
||||
this.findRegisteredNameCount();
|
||||
this.findRecentSponsorshipCount();
|
||||
|
||||
int score = this.calculateScore();
|
||||
if (score <= 0 && !override) {
|
||||
return;
|
||||
}
|
||||
|
||||
String newAddress = this.getDestinationAccount(this.address);
|
||||
while (newAddress != null) {
|
||||
// Found destination account
|
||||
this.penaltyAddresses.add(newAddress);
|
||||
|
||||
// Run algo for this address, but in "override" mode because it has already been flagged
|
||||
SelfSponsorshipAlgoV1 algoV1 = new SelfSponsorshipAlgoV1(this.repository, newAddress, this.snapshotTimestamp, true);
|
||||
algoV1.run();
|
||||
this.penaltyAddresses.addAll(algoV1.getPenaltyAddresses());
|
||||
|
||||
newAddress = this.getDestinationAccount(newAddress);
|
||||
}
|
||||
|
||||
this.penaltyAddresses.add(this.address);
|
||||
|
||||
if (this.override || this.recentSponsorshipCount < 20) {
|
||||
this.penaltyAddresses.addAll(this.consolidatedAddresses);
|
||||
this.penaltyAddresses.addAll(this.zeroTransactionAddreses);
|
||||
}
|
||||
else {
|
||||
this.penaltyAddresses.addAll(this.sponsees);
|
||||
}
|
||||
}
|
||||
|
||||
private String getDestinationAccount(String address) throws DataException {
|
||||
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||
if (transferPrivsTransactions.isEmpty()) {
|
||||
// No TRANSFER_PRIVS transactions for this address
|
||||
return null;
|
||||
}
|
||||
|
||||
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||
if (accountData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
|
||||
return transferPrivsTransactionData.getRecipient();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void findConsolidatedRewards() throws DataException {
|
||||
List<String> sponseesThatSentRewards = new ArrayList<>();
|
||||
Map<String, Integer> paymentRecipients = new HashMap<>();
|
||||
|
||||
// Collect outgoing payments of each sponsee
|
||||
for (String sponseeAddress : this.sponsees) {
|
||||
|
||||
// Firstly fetch all payments for address, since the functions below depend on this data
|
||||
this.fetchPaymentsForAddress(sponseeAddress);
|
||||
|
||||
// Check if the address has zero relevant transactions
|
||||
if (this.hasZeroTransactions(sponseeAddress)) {
|
||||
this.zeroTransactionAddreses.add(sponseeAddress);
|
||||
}
|
||||
|
||||
// Get payment recipients
|
||||
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||
if (allPaymentRecipients.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
sponseesThatSentRewards.add(sponseeAddress);
|
||||
|
||||
List<String> addressesPaidByThisSponsee = new ArrayList<>();
|
||||
for (String paymentRecipient : allPaymentRecipients) {
|
||||
if (addressesPaidByThisSponsee.contains(paymentRecipient)) {
|
||||
// We already tracked this association - don't allow multiple to stack up
|
||||
continue;
|
||||
}
|
||||
addressesPaidByThisSponsee.add(paymentRecipient);
|
||||
|
||||
// Increment count for this recipient, or initialize to 1 if not present
|
||||
if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) {
|
||||
paymentRecipients.put(paymentRecipient, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Exclude addresses with a low number of payments
|
||||
Map<String, Integer> filteredPaymentRecipients = paymentRecipients.entrySet().stream()
|
||||
.filter(p -> p.getValue() != null && p.getValue() >= 10)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
// Now check how many sponsees have sent to this subset of addresses
|
||||
Map<String, Integer> sponseesThatConsolidatedRewards = new HashMap<>();
|
||||
for (String sponseeAddress : sponseesThatSentRewards) {
|
||||
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||
// Remove any that aren't to one of the flagged recipients (i.e. consolidation)
|
||||
allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r));
|
||||
|
||||
int count = allPaymentRecipients.size();
|
||||
if (count == 0) {
|
||||
continue;
|
||||
}
|
||||
if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) {
|
||||
sponseesThatConsolidatedRewards.put(sponseeAddress, count);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove sponsees that have only sent a low number of payments to the filtered addresses
|
||||
Map<String, Integer> filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream()
|
||||
.filter(p -> p.getValue() != null && p.getValue() >= 2)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
this.consolidationCount = sponseesThatConsolidatedRewards.size();
|
||||
this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet());
|
||||
this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size();
|
||||
this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100);
|
||||
}
|
||||
|
||||
private void findBulkIssuance() {
|
||||
Long lastTimestamp = null;
|
||||
for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) {
|
||||
long timestamp = rewardShareTransactionData.getTimestamp();
|
||||
if (timestamp >= this.snapshotTimestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastTimestamp != null) {
|
||||
if (timestamp - lastTimestamp < 3*60*1000L) {
|
||||
this.bulkIssuanceCount++;
|
||||
}
|
||||
}
|
||||
lastTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
private void findRegisteredNameCount() throws DataException {
|
||||
int registeredNameCount = 0;
|
||||
for (String sponseeAddress : sponsees) {
|
||||
List<NameData> names = repository.getNameRepository().getNamesByOwner(sponseeAddress);
|
||||
for (NameData name : names) {
|
||||
if (name.getRegistered() < this.snapshotTimestamp) {
|
||||
registeredNameCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.registeredNameCount = registeredNameCount;
|
||||
}
|
||||
|
||||
private void findRecentSponsorshipCount() {
|
||||
final long referenceTimestamp = this.snapshotTimestamp - (365 * 24 * 60 * 60 * 1000L);
|
||||
int recentSponsorshipCount = 0;
|
||||
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
|
||||
if (rewardShare.getTimestamp() >= referenceTimestamp) {
|
||||
recentSponsorshipCount++;
|
||||
}
|
||||
}
|
||||
this.recentSponsorshipCount = recentSponsorshipCount;
|
||||
}
|
||||
|
||||
private int calculateScore() {
|
||||
final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1;
|
||||
final int nameMultiplier = (this.sponsees.size() >= 50 && this.registeredNameCount == 0) ? 2 : 1;
|
||||
final int consolidationMultiplier = Math.max(this.consolidationCount, 1);
|
||||
final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1);
|
||||
final int offset = 9;
|
||||
return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset;
|
||||
}
|
||||
|
||||
private void fetchSponsorshipRewardShares() throws DataException {
|
||||
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||
|
||||
// Define relevant transactions
|
||||
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
|
||||
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, this.address, false);
|
||||
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
|
||||
|
||||
// Skip removals
|
||||
if (rewardShareTransactionData.getSharePercent() < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not sponsored by this account
|
||||
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip self shares
|
||||
if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean duplicateFound = false;
|
||||
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
|
||||
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
|
||||
// Duplicate
|
||||
duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!duplicateFound) {
|
||||
sponsorshipRewardShares.add(rewardShareTransactionData);
|
||||
this.sponsees.add(rewardShareTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
|
||||
this.sponsorshipRewardShares = sponsorshipRewardShares;
|
||||
}
|
||||
|
||||
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
|
||||
return fetchTransactions(repository,
|
||||
List.of(TransactionType.TRANSFER_PRIVS),
|
||||
address, true);
|
||||
}
|
||||
|
||||
private void fetchPaymentsForAddress(String address) throws DataException {
|
||||
List<TransactionData> payments = fetchTransactions(repository,
|
||||
Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET),
|
||||
address, false);
|
||||
this.paymentsByAddress.put(address, payments);
|
||||
}
|
||||
|
||||
private List<String> fetchOutgoingPaymentRecipientsForAddress(String address) {
|
||||
List<String> outgoingPaymentRecipients = new ArrayList<>();
|
||||
|
||||
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||
if (transactionDataList == null) transactionDataList = new ArrayList<>();
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||
for (TransactionData transactionData : transactionDataList) {
|
||||
switch (transactionData.getType()) {
|
||||
|
||||
case PAYMENT:
|
||||
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
|
||||
if (!Objects.equals(paymentTransactionData.getRecipient(), address)) {
|
||||
// Outgoing payment from this account
|
||||
outgoingPaymentRecipients.add(paymentTransactionData.getRecipient());
|
||||
}
|
||||
break;
|
||||
|
||||
case TRANSFER_ASSET:
|
||||
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
|
||||
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
|
||||
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
|
||||
// Outgoing payment from this account
|
||||
outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient());
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return outgoingPaymentRecipients;
|
||||
}
|
||||
|
||||
private boolean hasZeroTransactions(String address) {
|
||||
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||
if (transactionDataList == null) {
|
||||
return true;
|
||||
}
|
||||
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||
return transactionDataList.size() == 0;
|
||||
}
|
||||
|
||||
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||
// Fetch all relevant transactions for this account
|
||||
List<byte[]> signatures = repository.getTransactionRepository()
|
||||
.getSignaturesMatchingCriteria(null, null, null, txTypes,
|
||||
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
|
||||
null, null, reverse);
|
||||
|
||||
List<TransactionData> transactionDataList = new ArrayList<>();
|
||||
|
||||
for (byte[] signature : signatures) {
|
||||
// Fetch transaction data
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null) {
|
||||
continue;
|
||||
}
|
||||
transactionDataList.add(transactionData);
|
||||
}
|
||||
|
||||
return transactionDataList;
|
||||
}
|
||||
|
||||
}
|
56
src/main/java/org/qortal/api/model/AccountPenaltyStats.java
Normal file
56
src/main/java/org/qortal/api/model/AccountPenaltyStats.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import org.qortal.block.SelfSponsorshipAlgoV1Block;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AccountPenaltyStats {
|
||||
|
||||
public Integer totalPenalties;
|
||||
public Integer maxPenalty;
|
||||
public Integer minPenalty;
|
||||
public String penaltyHash;
|
||||
|
||||
protected AccountPenaltyStats() {
|
||||
}
|
||||
|
||||
public AccountPenaltyStats(Integer totalPenalties, Integer maxPenalty, Integer minPenalty, String penaltyHash) {
|
||||
this.totalPenalties = totalPenalties;
|
||||
this.maxPenalty = maxPenalty;
|
||||
this.minPenalty = minPenalty;
|
||||
this.penaltyHash = penaltyHash;
|
||||
}
|
||||
|
||||
public static AccountPenaltyStats fromAccounts(List<AccountData> accounts) {
|
||||
int totalPenalties = 0;
|
||||
Integer maxPenalty = null;
|
||||
Integer minPenalty = null;
|
||||
|
||||
List<String> addresses = new ArrayList<>();
|
||||
for (AccountData accountData : accounts) {
|
||||
int penalty = accountData.getBlocksMintedPenalty();
|
||||
addresses.add(accountData.getAddress());
|
||||
totalPenalties++;
|
||||
|
||||
// Penalties are expressed as a negative number, so the min and the max are reversed here
|
||||
if (maxPenalty == null || penalty < maxPenalty) maxPenalty = penalty;
|
||||
if (minPenalty == null || penalty > minPenalty) minPenalty = penalty;
|
||||
}
|
||||
|
||||
String penaltyHash = SelfSponsorshipAlgoV1Block.getHash(addresses);
|
||||
return new AccountPenaltyStats(totalPenalties, maxPenalty, minPenalty, penaltyHash);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("totalPenalties: %d, maxPenalty: %d, minPenalty: %d, penaltyHash: %s", totalPenalties, maxPenalty, minPenalty, penaltyHash == null ? "null" : penaltyHash);
|
||||
}
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
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.network.Handshake;
|
||||
import org.qortal.network.Peer;
|
||||
@@ -36,6 +37,7 @@ public class ConnectedPeer {
|
||||
public Long lastBlockTimestamp;
|
||||
public UUID connectionId;
|
||||
public String age;
|
||||
public Boolean isTooDivergent;
|
||||
|
||||
protected ConnectedPeer() {
|
||||
}
|
||||
@@ -63,11 +65,16 @@ public class ConnectedPeer {
|
||||
this.age = "connecting...";
|
||||
}
|
||||
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
if (peerChainTipData != null) {
|
||||
this.lastHeight = peerChainTipData.getLastHeight();
|
||||
this.lastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
this.lastBlockTimestamp = peerChainTipData.getLastBlockTimestamp();
|
||||
this.lastHeight = peerChainTipData.getHeight();
|
||||
this.lastBlockSignature = peerChainTipData.getSignature();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -14,6 +14,7 @@ import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
@@ -27,6 +28,7 @@ import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.AccountPenaltyStats;
|
||||
import org.qortal.api.model.ApiOnlineAccount;
|
||||
import org.qortal.api.model.RewardShareKeyRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
@@ -34,6 +36,7 @@ import org.qortal.controller.LiteNode;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AccountPenaltyData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.data.network.OnlineAccountLevel;
|
||||
@@ -205,6 +208,10 @@ public class AddressesResource {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
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) {
|
||||
try {
|
||||
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
|
||||
@Path("/publicize")
|
||||
@Operation(
|
||||
|
@@ -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
|
||||
@Path("/apikey/generate")
|
||||
|
@@ -15,6 +15,7 @@ import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
@@ -44,6 +45,7 @@ import org.qortal.data.arbitrary.*;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -91,6 +93,7 @@ public class ArbitraryResource {
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@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 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");
|
||||
}
|
||||
|
||||
// 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()
|
||||
.getArbitraryResources(service, identifier, null, defaultRes, limit, offset, reverse);
|
||||
.getArbitraryResources(service, identifier, names, defaultRes, limit, offset, reverse);
|
||||
|
||||
if (resources == null) {
|
||||
return new ArrayList<>();
|
||||
@@ -216,7 +229,7 @@ public class ArbitraryResource {
|
||||
String name = creatorName.name;
|
||||
if (name != null) {
|
||||
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) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
|
@@ -69,6 +69,9 @@ public class ChatResource {
|
||||
public List<ChatMessage> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
|
||||
@QueryParam("txGroupId") Integer txGroupId,
|
||||
@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 = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
@@ -87,11 +90,22 @@ public class ChatResource {
|
||||
if (after != null && after < 1500000000000L)
|
||||
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()) {
|
||||
return repository.getChatRepository().getMessagesMatchingCriteria(
|
||||
before,
|
||||
after,
|
||||
txGroupId,
|
||||
referenceBytes,
|
||||
chatReferenceBytes,
|
||||
hasChatReference,
|
||||
involvingAddresses,
|
||||
limit, offset, reverse);
|
||||
} 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
|
||||
@Path("/active/{address}")
|
||||
@Operation(
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
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.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
@@ -284,6 +287,12 @@ public class CrossChainHtlcResource {
|
||||
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);
|
||||
if (crossChainTradeData == null) {
|
||||
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
|
||||
|
||||
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;
|
||||
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
|
||||
// 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
||||
TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
|
||||
if (tradeBotData == null)
|
||||
List<TradeBotData> tradeBotDataList = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).collect(Collectors.toList());
|
||||
if (tradeBotDataList == null || tradeBotDataList.isEmpty())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||
if (bitcoiny.getClass() == Bitcoin.class) {
|
||||
LOGGER.info("Refunding a Bitcoin HTLC is not yet supported");
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
// Loop through all matching entries for this AT address, as there might be more than one
|
||||
for (TradeBotData tradeBotData : tradeBotDataList) {
|
||||
|
||||
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
|
||||
if (NTP.getTime() <= lockTime * 1000L)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
|
||||
int lockTime = tradeBotData.getLockTimeA();
|
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTime)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||
// We can't refund P2SH-A until lockTime-A has passed
|
||||
if (NTP.getTime() <= lockTime * 1000L)
|
||||
continue;
|
||||
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTime)
|
||||
continue;
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// Still waiting for P2SH-A to be funded...
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
||||
// Create redeem script based on destination chain
|
||||
byte[] redeemScriptA;
|
||||
String p2shAddressA;
|
||||
BitcoinyHTLC.Status htlcStatusA;
|
||||
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:
|
||||
case REDEEMED:
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
// Too late!
|
||||
return false;
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// Still waiting for P2SH-A to be funded...
|
||||
continue;
|
||||
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
// Too late!
|
||||
continue;
|
||||
|
||||
// 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);
|
||||
case FUNDED: {
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
|
||||
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||
// Pirate Chain custom integration
|
||||
|
||||
bitcoiny.broadcastTransaction(p2shRefundTransaction);
|
||||
return true;
|
||||
PirateChain pirateChain = PirateChain.getInstance();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -46,6 +46,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
null,
|
||||
txGroupId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, null, null);
|
||||
|
||||
sendMessages(session, chatMessages);
|
||||
@@ -69,6 +72,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
@@ -2,10 +2,7 @@ package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
@@ -85,6 +82,7 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
final boolean excludeInitialData = queryParams.get("excludeInitialData") != null;
|
||||
|
||||
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
|
||||
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)
|
||||
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)
|
||||
tradeBotEntries = tradeBotEntries.stream()
|
||||
.filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Maybe send all known trade-bot entries
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<TradeBotData> tradeBotEntries = new ArrayList<>();
|
||||
|
||||
// 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)) {
|
||||
session.close(4002, "websocket issue");
|
||||
|
@@ -173,6 +173,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
|
||||
final boolean excludeInitialData = queryParams.get("excludeInitialData") != null;
|
||||
|
||||
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
|
||||
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<>();
|
||||
|
||||
synchronized (cachedInfoByBlockchain) {
|
||||
Collection<CachedOfferInfo> cachedInfos;
|
||||
// We might need to exclude the initial data from the response
|
||||
if (!excludeInitialData) {
|
||||
synchronized (cachedInfoByBlockchain) {
|
||||
Collection<CachedOfferInfo> cachedInfos;
|
||||
|
||||
if (foreignBlockchain == null)
|
||||
// No preferred blockchain, so iterate through all of them
|
||||
cachedInfos = cachedInfoByBlockchain.values();
|
||||
else
|
||||
cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
|
||||
if (foreignBlockchain == null)
|
||||
// No preferred blockchain, so iterate through all of them
|
||||
cachedInfos = cachedInfoByBlockchain.values();
|
||||
else
|
||||
cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
|
||||
|
||||
for (CachedOfferInfo cachedInfo : cachedInfos) {
|
||||
crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
|
||||
for (CachedOfferInfo cachedInfo : cachedInfos) {
|
||||
crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
|
||||
|
||||
if (includeHistoric)
|
||||
crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
|
||||
if (includeHistoric)
|
||||
crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -65,11 +65,15 @@ public class TradePresenceWebSocket extends ApiWebSocket implements Listener {
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
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) {
|
||||
tradePresences = List.copyOf(currentEntries.values());
|
||||
// We might need to exclude the initial data from the response
|
||||
if (!excludeInitialData) {
|
||||
synchronized (currentEntries) {
|
||||
tradePresences = List.copyOf(currentEntries.values());
|
||||
}
|
||||
}
|
||||
|
||||
if (!sendTradePresences(session, tradePresences)) {
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.arbitrary;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
@@ -88,7 +89,7 @@ public class ArbitraryDataBuilder {
|
||||
if (latestPut == null) {
|
||||
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
||||
this.name, this.service, this.identifierString());
|
||||
throw new DataException(message);
|
||||
throw new DataNotPublishedException(message);
|
||||
}
|
||||
this.latestPutTransaction = latestPut;
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||
@@ -169,10 +170,18 @@ public class ArbitraryDataReader {
|
||||
this.uncompress();
|
||||
this.validate();
|
||||
|
||||
} 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();
|
||||
throw e;
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("DataException when trying to load QDN resource", e);
|
||||
this.deleteWorkingDirectory();
|
||||
throw new DataException(e.getMessage());
|
||||
throw e;
|
||||
|
||||
} finally {
|
||||
this.postExecute();
|
||||
|
@@ -3,6 +3,7 @@ package org.qortal.arbitrary;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||
@@ -325,7 +326,7 @@ public class ArbitraryDataResource {
|
||||
if (latestPut == null) {
|
||||
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
||||
this.resourceId, this.service, this.identifierString());
|
||||
throw new DataException(message);
|
||||
throw new DataNotPublishedException(message);
|
||||
}
|
||||
this.latestPutTransaction = latestPut;
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -1,16 +1,16 @@
|
||||
package org.qortal.arbitrary.misc;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.arbitrary.ArbitraryDataRenderer;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.FilesystemUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
@@ -18,6 +18,31 @@ import static java.util.stream.Collectors.toMap;
|
||||
public enum Service {
|
||||
AUTO_UPDATE(1, false, null, null),
|
||||
ARBITRARY_DATA(100, false, null, null),
|
||||
QCHAT_ATTACHMENT(120, true, 1024*1024L, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) {
|
||||
// Custom validation function to require a single file, with a whitelisted extension
|
||||
int fileCount = 0;
|
||||
File[] files = path.toFile().listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
||||
}
|
||||
final String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
|
||||
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) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) {
|
||||
@@ -38,6 +63,7 @@ public enum Service {
|
||||
GIT_REPOSITORY(300, false, null, null),
|
||||
IMAGE(400, true, 10*1024*1024L, null),
|
||||
THUMBNAIL(410, true, 500*1024L, null),
|
||||
QCHAT_IMAGE(420, true, 500*1024L, null),
|
||||
VIDEO(500, false, null, null),
|
||||
AUDIO(600, false, null, null),
|
||||
BLOG(700, false, null, null),
|
||||
@@ -48,7 +74,30 @@ public enum Service {
|
||||
PLAYLIST(910, true, null, null),
|
||||
APP(1000, 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) {
|
||||
// Custom validation function to require .gif files only, and at least 1
|
||||
int gifCount = 0;
|
||||
File[] files = path.toFile().listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
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;
|
||||
private final boolean requiresValidation;
|
||||
@@ -114,7 +163,11 @@ public enum Service {
|
||||
OK(1),
|
||||
MISSING_KEYS(2),
|
||||
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;
|
||||
|
||||
|
@@ -89,7 +89,8 @@ public class Block {
|
||||
ONLINE_ACCOUNT_UNKNOWN(71),
|
||||
ONLINE_ACCOUNT_SIGNATURES_MISSING(72),
|
||||
ONLINE_ACCOUNT_SIGNATURES_MALFORMED(73),
|
||||
ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74);
|
||||
ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74),
|
||||
ONLINE_ACCOUNT_NONCE_INCORRECT(75);
|
||||
|
||||
public final int value;
|
||||
|
||||
@@ -135,7 +136,7 @@ public class Block {
|
||||
}
|
||||
|
||||
/** Lazy-instantiated expanded info on block's online accounts. */
|
||||
private static class ExpandedAccount {
|
||||
public static class ExpandedAccount {
|
||||
private final RewardShareData rewardShareData;
|
||||
private final int sharePercent;
|
||||
private final boolean isRecipientAlsoMinter;
|
||||
@@ -168,6 +169,13 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
public Account getMintingAccount() {
|
||||
return this.mintingAccount;
|
||||
}
|
||||
public Account getRecipientAccount() {
|
||||
return this.recipientAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns share bin for expanded account.
|
||||
* <p>
|
||||
@@ -362,13 +370,28 @@ public class Block {
|
||||
return null;
|
||||
}
|
||||
|
||||
int height = parentBlockData.getHeight() + 1;
|
||||
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
|
||||
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
|
||||
|
||||
// Fetch our list of online accounts
|
||||
// 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.error("No online accounts - not even our own?");
|
||||
LOGGER.debug("No online accounts - not even our own?");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -396,24 +419,36 @@ public class Block {
|
||||
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
|
||||
int onlineAccountsCount = onlineAccountsSet.size();
|
||||
|
||||
byte[] onlineAccountsSignatures;
|
||||
if (timestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
|
||||
// Collate all signatures
|
||||
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
|
||||
.stream()
|
||||
.map(OnlineAccountData::getSignature)
|
||||
.collect(Collectors.toList());
|
||||
// Collate all signatures
|
||||
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
|
||||
.stream()
|
||||
.map(OnlineAccountData::getSignature)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Aggregated, single signature
|
||||
onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
|
||||
} else {
|
||||
// Concatenate online account timestamp signatures (in correct order)
|
||||
onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH];
|
||||
// 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);
|
||||
System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
|
||||
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,
|
||||
@@ -421,7 +456,6 @@ public class Block {
|
||||
|
||||
int transactionCount = 0;
|
||||
byte[] transactionsSignature = null;
|
||||
int height = parentBlockData.getHeight() + 1;
|
||||
|
||||
int atCount = 0;
|
||||
long atFees = 0;
|
||||
@@ -1015,6 +1049,15 @@ public class Block {
|
||||
if (onlineRewardShares == null)
|
||||
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
|
||||
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
||||
if (this.blockData.getTimestamp() < signatureRequirementThreshold)
|
||||
@@ -1023,58 +1066,63 @@ public class Block {
|
||||
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
|
||||
|
||||
if (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
|
||||
// We expect just the one, aggregated signature
|
||||
if (this.blockData.getOnlineAccountsSignatures().length != Transformer.SIGNATURE_LENGTH)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
||||
} else {
|
||||
if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
|
||||
}
|
||||
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;
|
||||
|
||||
// Check signatures
|
||||
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
|
||||
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);
|
||||
|
||||
// Extract online accounts' timestamp signatures from block data. Only one signature if aggregated.
|
||||
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
|
||||
byte[] encodedOnlineAccountSignatures = this.blockData.getOnlineAccountsSignatures();
|
||||
|
||||
if (this.blockData.getTimestamp() >= BlockChain.getInstance().getAggregateSignatureTimestamp()) {
|
||||
// Aggregate all public keys
|
||||
Collection<byte[]> publicKeys = onlineRewardShares.stream()
|
||||
.map(RewardShareData::getRewardSharePublicKey)
|
||||
.collect(Collectors.toList());
|
||||
// Split online account signatures into signature(s) + nonces, then validate the nonces
|
||||
byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength);
|
||||
byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH);
|
||||
encodedOnlineAccountSignatures = extractedSignatures;
|
||||
|
||||
byte[] aggregatePublicKey = Qortal25519Extras.aggregatePublicKeys(publicKeys);
|
||||
List<Integer> nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
|
||||
|
||||
byte[] aggregateSignature = onlineAccountsSignatures.get(0);
|
||||
// Build block's view of online accounts (without signatures, as we don't need them here)
|
||||
Set<OnlineAccountData> onlineAccounts = new HashSet<>();
|
||||
for (int i = 0; i < onlineRewardShares.size(); ++i) {
|
||||
Integer nonce = nonces.get(i);
|
||||
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
|
||||
|
||||
// One-step verification of aggregate signature using aggregate public key
|
||||
if (!Qortal25519Extras.verifyAggregated(aggregatePublicKey, aggregateSignature, onlineTimestampBytes))
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
|
||||
} else {
|
||||
// Build block's view of online accounts
|
||||
Set<OnlineAccountData> onlineAccounts = new HashSet<>();
|
||||
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
|
||||
byte[] signature = onlineAccountsSignatures.get(i);
|
||||
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
|
||||
|
||||
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
|
||||
onlineAccounts.add(onlineAccountData);
|
||||
}
|
||||
|
||||
// Remove those already validated & cached by online accounts manager - no need to re-validate them
|
||||
OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
|
||||
|
||||
// Validate the rest
|
||||
for (OnlineAccountData onlineAccount : onlineAccounts)
|
||||
if (!Crypto.verify(onlineAccount.getPublicKey(), onlineAccount.getSignature(), onlineTimestampBytes))
|
||||
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
|
||||
|
||||
// We've validated these, so allow online accounts manager to cache
|
||||
OnlineAccountsManager.getInstance().addBlocksOnlineAccounts(onlineAccounts, onlineTimestamp);
|
||||
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, null, publicKey, nonce);
|
||||
onlineAccounts.add(onlineAccountData);
|
||||
}
|
||||
|
||||
// Remove those already validated & cached by online accounts manager - no need to re-validate them
|
||||
OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
|
||||
|
||||
// Validate the rest
|
||||
for (OnlineAccountData onlineAccount : onlineAccounts)
|
||||
if (!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
|
||||
this.cachedOnlineRewardShares = onlineRewardShares;
|
||||
|
||||
@@ -1415,6 +1463,9 @@ public class Block {
|
||||
if (this.blockData.getHeight() == 212937)
|
||||
// Apply fix for block 212937
|
||||
Block212937.processFix(this);
|
||||
|
||||
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||
SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
|
||||
}
|
||||
|
||||
// We're about to (test-)process a batch of transactions,
|
||||
@@ -1471,19 +1522,23 @@ public class Block {
|
||||
// Batch update in repository
|
||||
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
|
||||
for (AccountData accountData : allUniqueExpandedAccounts) {
|
||||
// Adjust count locally (in Java)
|
||||
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" : "")));
|
||||
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
if (newLevel > accountData.getLevel()) {
|
||||
// Account has increased in level!
|
||||
accountData.setLevel(newLevel);
|
||||
bumpedAccounts.put(accountData.getAddress(), newLevel);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
LOGGER.trace(() -> String.format("Block minter %s bumped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
}
|
||||
@@ -1491,6 +1546,25 @@ public class Block {
|
||||
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 {
|
||||
@@ -1650,6 +1724,9 @@ public class Block {
|
||||
// Revert fix for block 212937
|
||||
Block212937.orphanFix(this);
|
||||
|
||||
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
|
||||
|
||||
// Block rewards, including transaction fees, removed after transactions undone
|
||||
orphanBlockRewards();
|
||||
|
||||
@@ -1778,7 +1855,7 @@ public class Block {
|
||||
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" : "")));
|
||||
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
|
@@ -74,7 +74,11 @@ public class BlockChain {
|
||||
transactionV5Timestamp,
|
||||
transactionV6Timestamp,
|
||||
disableReferenceTimestamp,
|
||||
aggregateSignatureTimestamp;
|
||||
increaseOnlineAccountsDifficultyTimestamp,
|
||||
onlineAccountMinterLevelValidationHeight,
|
||||
selfSponsorshipAlgoV1Height,
|
||||
feeValidationFixTimestamp,
|
||||
chatReferenceTimestamp;
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@@ -96,6 +100,13 @@ public class BlockChain {
|
||||
/** Whether only one registered name is allowed per account. */
|
||||
private boolean oneNamePerAccount = false;
|
||||
|
||||
/** Checkpoints */
|
||||
public static class Checkpoint {
|
||||
public int height;
|
||||
public String signature;
|
||||
}
|
||||
private List<Checkpoint> checkpoints;
|
||||
|
||||
/** Block rewards by block height */
|
||||
public static class RewardByHeight {
|
||||
public int height;
|
||||
@@ -196,6 +207,9 @@ public class BlockChain {
|
||||
* 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;
|
||||
@@ -356,6 +370,11 @@ public class BlockChain {
|
||||
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. */
|
||||
public boolean getRequireGroupForApproval() {
|
||||
return this.requireGroupForApproval;
|
||||
@@ -369,6 +388,10 @@ public class BlockChain {
|
||||
return this.oneNamePerAccount;
|
||||
}
|
||||
|
||||
public List<Checkpoint> getCheckpoints() {
|
||||
return this.checkpoints;
|
||||
}
|
||||
|
||||
public List<RewardByHeight> getBlockRewardsByHeight() {
|
||||
return this.rewardsByHeight;
|
||||
}
|
||||
@@ -479,10 +502,27 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getAggregateSignatureTimestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.aggregateSignatureTimestamp.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
|
||||
|
||||
public long getRewardAtHeight(int ourHeight) {
|
||||
@@ -650,6 +690,7 @@ public class BlockChain {
|
||||
|
||||
boolean isTopOnly = Settings.getInstance().isTopOnly();
|
||||
boolean archiveEnabled = Settings.getInstance().isArchiveEnabled();
|
||||
boolean isLite = Settings.getInstance().isLite();
|
||||
boolean canBootstrap = Settings.getInstance().getBootstrap();
|
||||
boolean needsArchiveRebuild = false;
|
||||
BlockData chainTip;
|
||||
@@ -670,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) {
|
||||
// Top-only mode is enabled and we have blocks, so it's possible that the genesis block has been pruned
|
||||
// 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()));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,9 +757,7 @@ public class BlockChain {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
repository.checkConsistency();
|
||||
|
||||
// Set the number of blocks to validate based on the pruned state of the chain
|
||||
// If pruned, subtract an extra 10 to allow room for error
|
||||
int blocksToValidate = (isTopOnly || archiveEnabled) ? Settings.getInstance().getPruneBlockLimit() - 10 : 1440;
|
||||
int blocksToValidate = Math.min(Settings.getInstance().getPruneBlockLimit() - 10, 1440);
|
||||
|
||||
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - blocksToValidate, 1);
|
||||
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
|
||||
|
133
src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
Normal file
133
src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package org.qortal.block;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.SelfSponsorshipAlgoV1;
|
||||
import org.qortal.api.model.AccountPenaltyStats;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.AccountPenaltyData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Self Sponsorship AlgoV1 Block
|
||||
* <p>
|
||||
* Selected block for the initial run on the "self sponsorship detection algorithm"
|
||||
*/
|
||||
public final class SelfSponsorshipAlgoV1Block {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV1Block.class);
|
||||
|
||||
|
||||
private SelfSponsorshipAlgoV1Block() {
|
||||
/* Do not instantiate */
|
||||
}
|
||||
|
||||
public static void processAccountPenalties(Block block) throws DataException {
|
||||
LOGGER.info("Running algo for block processing - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, -5000000);
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
}
|
||||
|
||||
public static void orphanAccountPenalties(Block block) throws DataException {
|
||||
LOGGER.info("Running algo for block orphaning - this will take a while...");
|
||||
logPenaltyStats(block.repository);
|
||||
long startTime = System.currentTimeMillis();
|
||||
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, 5000000);
|
||||
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||
long totalTime = System.currentTimeMillis() - startTime;
|
||||
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||
LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||
logPenaltyStats(block.repository);
|
||||
|
||||
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||
}
|
||||
|
||||
public static Set<AccountPenaltyData> getAccountPenalties(Repository repository, int penalty) throws DataException {
|
||||
final long snapshotTimestamp = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
|
||||
Set<AccountPenaltyData> penalties = new LinkedHashSet<>();
|
||||
List<String> addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares();
|
||||
for (String address : addresses) {
|
||||
//System.out.println(String.format("address: %s", address));
|
||||
SelfSponsorshipAlgoV1 selfSponsorshipAlgoV1 = new SelfSponsorshipAlgoV1(repository, address, snapshotTimestamp, false);
|
||||
selfSponsorshipAlgoV1.run();
|
||||
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV1.getPenaltyAddresses().size()));
|
||||
|
||||
for (String penaltyAddress : selfSponsorshipAlgoV1.getPenaltyAddresses()) {
|
||||
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
|
||||
}
|
||||
}
|
||||
return penalties;
|
||||
}
|
||||
|
||||
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||
|
||||
int updatedCount = 0;
|
||||
|
||||
for (AccountPenaltyData penaltyData : accountPenalties) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
|
||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||
|
||||
// Shortcut for penalties
|
||||
if (effectiveBlocksMinted < 0) {
|
||||
accountData.setLevel(0);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
|
||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||
accountData.setLevel(newLevel);
|
||||
repository.getAccountRepository().setLevel(accountData);
|
||||
updatedCount++;
|
||||
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
private static void logPenaltyStats(Repository repository) {
|
||||
try {
|
||||
LOGGER.info(getPenaltyStats(repository));
|
||||
|
||||
} catch (DataException e) {}
|
||||
}
|
||||
|
||||
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
|
||||
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||
return AccountPenaltyStats.fromAccounts(accounts);
|
||||
}
|
||||
|
||||
public static String getHash(List<String> penaltyAddresses) {
|
||||
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Collections.sort(penaltyAddresses);
|
||||
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
}
|
@@ -35,6 +35,8 @@ import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
// Minting new blocks
|
||||
|
||||
public class BlockMinter extends Thread {
|
||||
@@ -61,8 +63,8 @@ public class BlockMinter extends Thread {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("BlockMinter");
|
||||
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes do not mint
|
||||
if (Settings.getInstance().isTopOnly() || Settings.getInstance().isLite()) {
|
||||
// Top only and lite nodes do not sign blocks
|
||||
return;
|
||||
}
|
||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||
@@ -90,6 +92,8 @@ public class BlockMinter extends Thread {
|
||||
|
||||
List<Block> newBlocks = new ArrayList<>();
|
||||
|
||||
final boolean isSingleNodeTestnet = Settings.getInstance().isSingleNodeTestnet();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Going to need this a lot...
|
||||
BlockRepository blockRepository = repository.getBlockRepository();
|
||||
@@ -108,8 +112,9 @@ public class BlockMinter extends Thread {
|
||||
// Free up any repository locks
|
||||
repository.discardChanges();
|
||||
|
||||
// Sleep for a while
|
||||
Thread.sleep(1000);
|
||||
// Sleep for a while.
|
||||
// It's faster on single node testnets, to allow lots of blocks to be minted quickly.
|
||||
Thread.sleep(isSingleNodeTestnet ? 50 : 1000);
|
||||
|
||||
isMintingPossible = false;
|
||||
|
||||
@@ -220,9 +225,10 @@ public class BlockMinter extends Thread {
|
||||
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) {
|
||||
if (mintedLastBlock && !isSingleNodeTestnet) {
|
||||
LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
|
||||
continue;
|
||||
}
|
||||
@@ -241,7 +247,7 @@ public class BlockMinter extends Thread {
|
||||
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"));
|
||||
moderatedLog(() -> LOGGER.info("Couldn't build a to-be-minted block"));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -433,11 +439,9 @@ public class BlockMinter extends Thread {
|
||||
|
||||
if (newBlockMinted) {
|
||||
// Broadcast our new chain to network
|
||||
BlockData newBlockData = newBlock.getBlockData();
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
|
||||
Network.getInstance().broadcastOurChain();
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// We've been interrupted - time to exit
|
||||
return;
|
||||
@@ -506,6 +510,21 @@ public class BlockMinter extends Thread {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -513,6 +532,8 @@ public class BlockMinter extends Thread {
|
||||
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
||||
|
||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||
if (newBlock == null)
|
||||
return null;
|
||||
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
@@ -29,6 +29,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.api.ApiService;
|
||||
import org.qortal.api.DomainMapService;
|
||||
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.BlockSummaryData;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -317,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. */
|
||||
public BlockData getChainTip() {
|
||||
synchronized (this.latestBlocks) {
|
||||
@@ -727,25 +731,25 @@ public class Controller extends Thread {
|
||||
|
||||
public static final Predicate<Peer> hasNoRecentBlock = peer -> {
|
||||
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp;
|
||||
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp;
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasNoOrSameBlock = peer -> {
|
||||
final BlockData latestBlockData = getInstance().getChainTip();
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getLastBlockSignature());
|
||||
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getSignature());
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasOnlyGenesisBlock = peer -> {
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getLastHeight() == null || peerChainTipData.getLastHeight() == 1;
|
||||
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
return peerChainTipData == null || peerChainTipData.getHeight() == 1;
|
||||
};
|
||||
|
||||
public static final Predicate<Peer> hasInferiorChainTip = peer -> {
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
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 -> {
|
||||
@@ -753,6 +757,28 @@ public class Controller extends Thread {
|
||||
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() {
|
||||
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
|
||||
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
|
||||
@@ -835,6 +861,12 @@ public class Controller extends Thread {
|
||||
String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText);
|
||||
if (!Settings.getInstance().isLite()) {
|
||||
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));
|
||||
SysTray.getInstance().setToolTipText(tooltip);
|
||||
@@ -1007,8 +1039,7 @@ public class Controller extends Thread {
|
||||
network.broadcast(peer -> peer.isOutbound() ? network.buildPeersMessage(peer) : new GetPeersMessage());
|
||||
|
||||
// Send our current height
|
||||
BlockData latestBlockData = getChainTip();
|
||||
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
|
||||
network.broadcastOurChain();
|
||||
|
||||
// Request unconfirmed transaction signatures, but only if we're up-to-date.
|
||||
// If we're NOT up-to-date then priority is synchronizing first
|
||||
@@ -1215,6 +1246,10 @@ public class Controller extends Thread {
|
||||
onNetworkHeightV2Message(peer, message);
|
||||
break;
|
||||
|
||||
case BLOCK_SUMMARIES_V2:
|
||||
onNetworkBlockSummariesV2Message(peer, message);
|
||||
break;
|
||||
|
||||
case GET_TRANSACTION:
|
||||
TransactionImporter.getInstance().onNetworkGetTransactionMessage(peer, message);
|
||||
break;
|
||||
@@ -1232,25 +1267,20 @@ public class Controller extends Thread {
|
||||
break;
|
||||
|
||||
case GET_ONLINE_ACCOUNTS:
|
||||
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsMessage(peer, message);
|
||||
break;
|
||||
|
||||
case ONLINE_ACCOUNTS:
|
||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ONLINE_ACCOUNTS_V2:
|
||||
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV2Message(peer, message);
|
||||
break;
|
||||
|
||||
case ONLINE_ACCOUNTS_V2:
|
||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV2Message(peer, message);
|
||||
// No longer supported - to be eventually removed
|
||||
break;
|
||||
|
||||
case GET_ONLINE_ACCOUNTS_V3:
|
||||
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message);
|
||||
break;
|
||||
|
||||
case ONLINE_ACCOUNTS_V3:
|
||||
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ARBITRARY_DATA:
|
||||
// Not currently supported
|
||||
break;
|
||||
@@ -1374,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
|
||||
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
|
||||
Message blockUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
// Send generic 'unknown' message as it's very short
|
||||
Message blockUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
|
||||
? new GenericUnknownMessage()
|
||||
: new BlockSummariesMessage(Collections.emptyList());
|
||||
blockUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(blockUnknownMessage))
|
||||
peer.disconnect("failed to send block-unknown response");
|
||||
@@ -1424,11 +1456,15 @@ public class Controller extends Thread {
|
||||
this.stats.getBlockSummariesStats.requests.incrementAndGet();
|
||||
|
||||
// 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();
|
||||
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());
|
||||
|
||||
if (!peer.sendMessage(blockSummariesMessage))
|
||||
peer.disconnect("failed to send block summaries");
|
||||
|
||||
@@ -1484,7 +1520,9 @@ public class Controller extends Thread {
|
||||
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());
|
||||
if (!peer.sendMessage(blockSummariesMessage))
|
||||
peer.disconnect("failed to send block summaries");
|
||||
@@ -1559,18 +1597,59 @@ public class Controller extends Thread {
|
||||
// If peer is inbound and we've not updated their height
|
||||
// then this is probably their initial HEIGHT_V2 message
|
||||
// so they need a corresponding HEIGHT_V2 message from us
|
||||
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
|
||||
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
|
||||
if (!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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Potentially synchronize
|
||||
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) {
|
||||
GetAccountMessage getAccountMessage = (GetAccountMessage) message;
|
||||
String address = getAccountMessage.getAddress();
|
||||
@@ -1586,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
|
||||
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s", peer, address));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
// Send generic 'unknown' message as it's very short
|
||||
Message accountUnknownMessage = new GenericUnknownMessage();
|
||||
accountUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(accountUnknownMessage))
|
||||
peer.disconnect("failed to send account-unknown response");
|
||||
@@ -1622,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
|
||||
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_BALANCE request for unknown account %s and asset ID %d", peer, address, assetId));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
// Send generic 'unknown' message as it's very short
|
||||
Message accountUnknownMessage = new GenericUnknownMessage();
|
||||
accountUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(accountUnknownMessage))
|
||||
peer.disconnect("failed to send account-unknown response");
|
||||
@@ -1666,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
|
||||
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_TRANSACTIONS request for unknown account %s", peer, address));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
// Send generic 'unknown' message as it's very short
|
||||
Message accountUnknownMessage = new GenericUnknownMessage();
|
||||
accountUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(accountUnknownMessage))
|
||||
peer.disconnect("failed to send account-unknown response");
|
||||
@@ -1703,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
|
||||
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_NAMES request for unknown account %s", peer, address));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
// Send generic 'unknown' message as it's very short
|
||||
Message accountUnknownMessage = new GenericUnknownMessage();
|
||||
accountUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(accountUnknownMessage))
|
||||
peer.disconnect("failed to send account-unknown response");
|
||||
@@ -1738,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
|
||||
LOGGER.debug(() -> String.format("Sending 'name unknown' response to peer %s for GET_NAME request for unknown name %s", peer, name));
|
||||
|
||||
// We'll send empty block summaries message as it's very short
|
||||
Message nameUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
// Send generic 'unknown' message as it's very short
|
||||
Message nameUnknownMessage = new GenericUnknownMessage();
|
||||
nameUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(nameUnknownMessage))
|
||||
peer.disconnect("failed to send name-unknown response");
|
||||
@@ -1787,14 +1866,14 @@ public class Controller extends Thread {
|
||||
continue;
|
||||
}
|
||||
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
if (peerChainTipData == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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();
|
||||
continue;
|
||||
}
|
||||
@@ -1822,6 +1901,10 @@ public class Controller extends Thread {
|
||||
if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp)
|
||||
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
|
||||
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||
if (peers == null)
|
||||
|
@@ -9,6 +9,7 @@ import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.crypto.Qortal25519Extras;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
@@ -19,10 +20,13 @@ import org.qortal.network.message.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.NamedThreadFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -49,13 +53,30 @@ public class OnlineAccountsManager {
|
||||
*/
|
||||
private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 3;
|
||||
|
||||
private static final long ONLINE_ACCOUNTS_QUEUE_INTERVAL = 100L; //ms
|
||||
private static final long ONLINE_ACCOUNTS_QUEUE_INTERVAL = 100L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL = 60 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 15 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_COMPUTE_INTERVAL = 5 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 60 * 1000L; // ms
|
||||
// After switching to a new online timestamp, we "burst" the online accounts requests
|
||||
// at an increased interval for a specified amount of time
|
||||
private static final long ONLINE_ACCOUNTS_BROADCAST_BURST_INTERVAL = 5 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_BROADCAST_BURST_LENGTH = 5 * 60 * 1000L; // ms
|
||||
|
||||
private static final long ONLINE_ACCOUNTS_V2_PEER_VERSION = 0x0300020000L; // v3.2.0
|
||||
private static final long ONLINE_ACCOUNTS_V3_PEER_VERSION = 0x0300040000L; // v3.4.0
|
||||
private static final long ONLINE_ACCOUNTS_COMPUTE_INITIAL_SLEEP_INTERVAL = 30 * 1000L; // ms
|
||||
|
||||
// MemoryPoW - mainnet
|
||||
public static final int POW_BUFFER_SIZE = 1 * 1024 * 1024; // bytes
|
||||
public static final int POW_DIFFICULTY_V1 = 18; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_V2 = 19; // leading zero bits
|
||||
|
||||
// MemoryPoW - testnet
|
||||
public static final int POW_BUFFER_SIZE_TESTNET = 1 * 1024 * 1024; // bytes
|
||||
public static final int POW_DIFFICULTY_TESTNET = 5; // leading zero bits
|
||||
|
||||
// IMPORTANT: if we ever need to dynamically modify the buffer size using a feature trigger, the
|
||||
// pre-allocated buffer below will NOT work, and we should instead use a dynamically allocated
|
||||
// one for the transition period.
|
||||
private static long[] POW_VERIFY_WORK_BUFFER = new long[getPoWBufferSize() / 8];
|
||||
|
||||
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4, new NamedThreadFactory("OnlineAccounts"));
|
||||
private volatile boolean isStopping = false;
|
||||
@@ -77,6 +98,8 @@ public class OnlineAccountsManager {
|
||||
*/
|
||||
private final SortedMap<Long, Set<OnlineAccountData>> latestBlocksOnlineAccounts = new ConcurrentSkipListMap<>();
|
||||
|
||||
private long lastOnlineAccountsRequest = 0;
|
||||
|
||||
private boolean hasOurOnlineAccounts = false;
|
||||
|
||||
public static long getOnlineTimestampModulus() {
|
||||
@@ -95,6 +118,27 @@ public class OnlineAccountsManager {
|
||||
return (now / onlineTimestampModulus) * onlineTimestampModulus;
|
||||
}
|
||||
|
||||
public static long toOnlineAccountTimestamp(long timestamp) {
|
||||
return (timestamp / getOnlineTimestampModulus()) * getOnlineTimestampModulus();
|
||||
}
|
||||
|
||||
private static int getPoWBufferSize() {
|
||||
if (Settings.getInstance().isTestNet())
|
||||
return POW_BUFFER_SIZE_TESTNET;
|
||||
|
||||
return POW_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
private static int getPoWDifficulty(long timestamp) {
|
||||
if (Settings.getInstance().isTestNet())
|
||||
return POW_DIFFICULTY_TESTNET;
|
||||
|
||||
if (timestamp >= BlockChain.getInstance().getIncreaseOnlineAccountsDifficultyTimestamp())
|
||||
return POW_DIFFICULTY_V2;
|
||||
|
||||
return POW_DIFFICULTY_V1;
|
||||
}
|
||||
|
||||
private OnlineAccountsManager() {
|
||||
}
|
||||
|
||||
@@ -110,16 +154,16 @@ public class OnlineAccountsManager {
|
||||
// Expire old online accounts signatures
|
||||
executor.scheduleAtFixedRate(this::expireOldOnlineAccounts, ONLINE_ACCOUNTS_TASKS_INTERVAL, ONLINE_ACCOUNTS_TASKS_INTERVAL, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Send our online accounts
|
||||
executor.scheduleAtFixedRate(this::sendOurOnlineAccountsInfo, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Request online accounts from peers (legacy)
|
||||
executor.scheduleAtFixedRate(this::requestLegacyRemoteOnlineAccounts, ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
|
||||
// Request online accounts from peers (V3+)
|
||||
executor.scheduleAtFixedRate(this::requestRemoteOnlineAccounts, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
|
||||
// Request online accounts from peers
|
||||
executor.scheduleAtFixedRate(this::requestRemoteOnlineAccounts, ONLINE_ACCOUNTS_BROADCAST_BURST_INTERVAL, ONLINE_ACCOUNTS_BROADCAST_BURST_INTERVAL, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Process import queue
|
||||
executor.scheduleWithFixedDelay(this::processOnlineAccountsImportQueue, ONLINE_ACCOUNTS_QUEUE_INTERVAL, ONLINE_ACCOUNTS_QUEUE_INTERVAL, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Send our online accounts (using increased initial delay)
|
||||
// This allows some time for initial online account lists to be retrieved, and
|
||||
// reduces the chances of the same nonce being computed twice
|
||||
executor.scheduleAtFixedRate(this::sendOurOnlineAccountsInfo, ONLINE_ACCOUNTS_COMPUTE_INITIAL_SLEEP_INTERVAL, ONLINE_ACCOUNTS_COMPUTE_INTERVAL, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
@@ -139,18 +183,17 @@ public class OnlineAccountsManager {
|
||||
return;
|
||||
|
||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||
final boolean useAggregateCompatibleSignature = onlineAccountsTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp();
|
||||
|
||||
Set<OnlineAccountData> replacementAccounts = new HashSet<>();
|
||||
for (PrivateKeyAccount onlineAccount : onlineAccounts) {
|
||||
// Check mintingAccount is actually reward-share?
|
||||
|
||||
byte[] signature = useAggregateCompatibleSignature
|
||||
? Qortal25519Extras.signForAggregation(onlineAccount.getPrivateKey(), timestampBytes)
|
||||
: onlineAccount.sign(timestampBytes);
|
||||
byte[] signature = Qortal25519Extras.signForAggregation(onlineAccount.getPrivateKey(), timestampBytes);
|
||||
byte[] publicKey = onlineAccount.getPublicKey();
|
||||
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
||||
Integer nonce = new Random().nextInt(500000);
|
||||
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, nonce);
|
||||
replacementAccounts.add(ourOnlineAccountData);
|
||||
}
|
||||
|
||||
@@ -168,28 +211,86 @@ public class OnlineAccountsManager {
|
||||
LOGGER.debug("Processing online accounts import queue (size: {})", this.onlineAccountsImportQueue.size());
|
||||
|
||||
Set<OnlineAccountData> onlineAccountsToAdd = new HashSet<>();
|
||||
Set<OnlineAccountData> onlineAccountsToRemove = new HashSet<>();
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) {
|
||||
if (isStopping)
|
||||
return;
|
||||
|
||||
// Skip this account if it's already validated
|
||||
Set<OnlineAccountData> onlineAccounts = this.currentOnlineAccounts.get(onlineAccountData.getTimestamp());
|
||||
if (onlineAccounts != null && onlineAccounts.contains(onlineAccountData)) {
|
||||
// We have already validated this online account
|
||||
onlineAccountsImportQueue.remove(onlineAccountData);
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isValid = this.isValidCurrentAccount(repository, onlineAccountData);
|
||||
if (isValid)
|
||||
onlineAccountsToAdd.add(onlineAccountData);
|
||||
|
||||
// Remove from queue
|
||||
onlineAccountsImportQueue.remove(onlineAccountData);
|
||||
// Don't remove from the queue yet - we'll do this at the end of the process
|
||||
// This prevents duplicates being added to the queue whilst it's being processed
|
||||
onlineAccountsToRemove.add(onlineAccountData);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue while verifying online accounts", e);
|
||||
}
|
||||
|
||||
if (!onlineAccountsToAdd.isEmpty()) {
|
||||
LOGGER.debug("Merging {} validated online accounts from import queue", onlineAccountsToAdd.size());
|
||||
addAccounts(onlineAccountsToAdd);
|
||||
} finally {
|
||||
if (!onlineAccountsToAdd.isEmpty()) {
|
||||
LOGGER.debug("Merging {} validated online accounts from import queue", onlineAccountsToAdd.size());
|
||||
addAccounts(onlineAccountsToAdd);
|
||||
}
|
||||
onlineAccountsImportQueue.removeAll(onlineAccountsToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if supplied onlineAccountData is superior (i.e. has a nonce value) than existing record.
|
||||
* Two entries are considered equal even if the nonce differs, to prevent multiple variations
|
||||
* co-existing. For this reason, we need to be able to check if a new OnlineAccountData entry should
|
||||
* replace the existing one, which may be missing the nonce.
|
||||
* @param onlineAccountData
|
||||
* @return true if supplied data is superior to existing entry
|
||||
*/
|
||||
private boolean isOnlineAccountsDataSuperior(OnlineAccountData onlineAccountData) {
|
||||
if (onlineAccountData.getNonce() == null || onlineAccountData.getNonce() < 0) {
|
||||
// New online account data has no usable nonce value, so it won't be better than anything we already have
|
||||
return false;
|
||||
}
|
||||
|
||||
// New online account data has a nonce value, so check if there is any existing data to compare against
|
||||
Set<OnlineAccountData> existingOnlineAccountsForTimestamp = this.currentOnlineAccounts.get(onlineAccountData.getTimestamp());
|
||||
if (existingOnlineAccountsForTimestamp == null) {
|
||||
// No existing online accounts data with this timestamp yet
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if a duplicate entry exists
|
||||
OnlineAccountData existingOnlineAccountData = null;
|
||||
for (OnlineAccountData existingAccount : existingOnlineAccountsForTimestamp) {
|
||||
if (existingAccount.equals(onlineAccountData)) {
|
||||
// Found existing online account data
|
||||
existingOnlineAccountData = existingAccount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingOnlineAccountData == null) {
|
||||
// No existing online accounts data, so nothing to compare
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingOnlineAccountData.getNonce() == null || existingOnlineAccountData.getNonce() < 0) {
|
||||
// Existing data has no usable nonce value(s) so we want to replace it with the new one
|
||||
return true;
|
||||
}
|
||||
|
||||
// Both new and old data have nonce values so the new data isn't considered superior
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Utilities
|
||||
|
||||
public static byte[] xorByteArrayInPlace(byte[] inplaceArray, byte[] otherArray) {
|
||||
@@ -225,9 +326,7 @@ public class OnlineAccountsManager {
|
||||
|
||||
// Verify signature
|
||||
byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp());
|
||||
boolean isSignatureValid = onlineAccountTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp()
|
||||
? Qortal25519Extras.verifyAggregated(rewardSharePublicKey, onlineAccountData.getSignature(), data)
|
||||
: Crypto.verify(rewardSharePublicKey, onlineAccountData.getSignature(), data);
|
||||
boolean isSignatureValid = Qortal25519Extras.verifyAggregated(rewardSharePublicKey, onlineAccountData.getSignature(), data);
|
||||
if (!isSignatureValid) {
|
||||
LOGGER.trace(() -> String.format("Rejecting invalid online account %s", Base58.encode(rewardSharePublicKey)));
|
||||
return false;
|
||||
@@ -248,6 +347,12 @@ public class OnlineAccountsManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate mempow
|
||||
if (!getInstance().verifyMemoryPoW(onlineAccountData, POW_VERIFY_WORK_BUFFER)) {
|
||||
LOGGER.trace(() -> String.format("Rejecting online reward-share for account %s due to invalid PoW nonce", mintingAccount.getAddress()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -269,7 +374,7 @@ public class OnlineAccountsManager {
|
||||
for (var entry : hashesToRebuild.entrySet()) {
|
||||
Long timestamp = entry.getKey();
|
||||
|
||||
LOGGER.debug(() -> String.format("Rehashing for timestamp %d and leading bytes %s",
|
||||
LOGGER.trace(() -> String.format("Rehashing for timestamp %d and leading bytes %s",
|
||||
timestamp,
|
||||
entry.getValue().stream().sorted(Byte::compareUnsigned).map(leadingByte -> String.format("%02x", leadingByte)).collect(Collectors.joining(", "))
|
||||
)
|
||||
@@ -295,7 +400,7 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("we have online accounts for timestamps: %s", String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
|
||||
LOGGER.trace(String.format("we have online accounts for timestamps: %s", String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -305,6 +410,12 @@ public class OnlineAccountsManager {
|
||||
long onlineAccountTimestamp = onlineAccountData.getTimestamp();
|
||||
|
||||
Set<OnlineAccountData> onlineAccounts = this.currentOnlineAccounts.computeIfAbsent(onlineAccountTimestamp, k -> ConcurrentHashMap.newKeySet());
|
||||
|
||||
boolean isSuperiorEntry = isOnlineAccountsDataSuperior(onlineAccountData);
|
||||
if (isSuperiorEntry)
|
||||
// Remove existing inferior entry so it can be re-added below (it's likely the existing copy is missing a nonce value)
|
||||
onlineAccounts.remove(onlineAccountData);
|
||||
|
||||
boolean isNewEntry = onlineAccounts.add(onlineAccountData);
|
||||
|
||||
if (isNewEntry)
|
||||
@@ -329,30 +440,7 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Request data from other peers. (Pre-V3)
|
||||
*/
|
||||
private void requestLegacyRemoteOnlineAccounts() {
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
return;
|
||||
|
||||
// Don't bother if we're not up to date
|
||||
if (!Controller.getInstance().isUpToDate())
|
||||
return;
|
||||
|
||||
List<OnlineAccountData> mergedOnlineAccounts = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
|
||||
|
||||
Message messageV2 = new GetOnlineAccountsV2Message(mergedOnlineAccounts);
|
||||
|
||||
Network.getInstance().broadcast(peer ->
|
||||
peer.getPeersVersion() < ONLINE_ACCOUNTS_V3_PEER_VERSION
|
||||
? messageV2
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request data from other peers. V3+
|
||||
* Request data from other peers
|
||||
*/
|
||||
private void requestRemoteOnlineAccounts() {
|
||||
final Long now = NTP.getTime();
|
||||
@@ -363,13 +451,25 @@ public class OnlineAccountsManager {
|
||||
if (!Controller.getInstance().isUpToDate())
|
||||
return;
|
||||
|
||||
Message messageV3 = new GetOnlineAccountsV3Message(currentOnlineAccountsHashes);
|
||||
long onlineAccountsTimestamp = getCurrentOnlineAccountTimestamp();
|
||||
if (now - onlineAccountsTimestamp >= ONLINE_ACCOUNTS_BROADCAST_BURST_LENGTH) {
|
||||
// New online timestamp started more than 5 mins ago - we probably don't need to request so frequently
|
||||
|
||||
Network.getInstance().broadcast(peer ->
|
||||
peer.getPeersVersion() >= ONLINE_ACCOUNTS_V3_PEER_VERSION
|
||||
? messageV3
|
||||
: null
|
||||
);
|
||||
if (Controller.uptime() < ONLINE_ACCOUNTS_BROADCAST_BURST_LENGTH) {
|
||||
// The node recently started up, so we should request at the burst interval
|
||||
// This could allow accounts to move around the network more easily when an auto update is occurring
|
||||
}
|
||||
else if (now - lastOnlineAccountsRequest < ONLINE_ACCOUNTS_BROADCAST_INTERVAL) {
|
||||
// We already requested online accounts in the last minute, so no need to request again
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("Requesting online accounts via broadcast...");
|
||||
|
||||
lastOnlineAccountsRequest = now;
|
||||
Message messageV3 = new GetOnlineAccountsV3Message(currentOnlineAccountsHashes);
|
||||
Network.getInstance().broadcast(peer -> messageV3);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,13 +492,26 @@ public class OnlineAccountsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'next' timestamp (prioritize this as it's the most important, if mempow active)
|
||||
final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(now) + getOnlineTimestampModulus();
|
||||
boolean success = computeOurAccountsForTimestamp(nextOnlineAccountsTimestamp);
|
||||
if (!success) {
|
||||
// We didn't compute the required nonce value(s), and so can't proceed until they have been retried
|
||||
return;
|
||||
}
|
||||
|
||||
// 'current' timestamp
|
||||
computeOurAccountsForTimestamp(onlineAccountsTimestamp);
|
||||
}
|
||||
|
||||
private boolean computeOurAccountsForTimestamp(long onlineAccountsTimestamp) {
|
||||
List<MintingAccountData> mintingAccounts;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
mintingAccounts = repository.getAccountRepository().getMintingAccounts();
|
||||
|
||||
// We have no accounts to send
|
||||
if (mintingAccounts.isEmpty())
|
||||
return;
|
||||
return false;
|
||||
|
||||
// Only active reward-shares allowed
|
||||
Iterator<MintingAccountData> iterator = mintingAccounts.iterator();
|
||||
@@ -421,25 +534,66 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to fetch minting accounts: %s", e.getMessage()));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean useAggregateCompatibleSignature = onlineAccountsTimestamp >= BlockChain.getInstance().getAggregateSignatureTimestamp();
|
||||
|
||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
|
||||
|
||||
int remaining = mintingAccounts.size();
|
||||
for (MintingAccountData mintingAccountData : mintingAccounts) {
|
||||
remaining--;
|
||||
byte[] privateKey = mintingAccountData.getPrivateKey();
|
||||
byte[] publicKey = Crypto.toPublicKey(privateKey);
|
||||
|
||||
byte[] signature = useAggregateCompatibleSignature
|
||||
? Qortal25519Extras.signForAggregation(privateKey, timestampBytes)
|
||||
: Crypto.sign(privateKey, timestampBytes);
|
||||
// We don't want to compute the online account nonce and signature again if it already exists
|
||||
Set<OnlineAccountData> onlineAccounts = this.currentOnlineAccounts.computeIfAbsent(onlineAccountsTimestamp, k -> ConcurrentHashMap.newKeySet());
|
||||
boolean alreadyExists = onlineAccounts.stream().anyMatch(a -> Arrays.equals(a.getPublicKey(), publicKey));
|
||||
if (alreadyExists) {
|
||||
this.hasOurOnlineAccounts = true;
|
||||
|
||||
if (remaining > 0) {
|
||||
// Move on to next account
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
// Everything exists, so return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate bytes for mempow
|
||||
byte[] mempowBytes;
|
||||
try {
|
||||
mempowBytes = this.getMemoryPoWBytes(publicKey, onlineAccountsTimestamp);
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.info("Unable to create bytes for MemoryPoW. Moving on to next account...");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute nonce
|
||||
Integer nonce;
|
||||
try {
|
||||
nonce = this.computeMemoryPoW(mempowBytes, publicKey, onlineAccountsTimestamp);
|
||||
if (nonce == null) {
|
||||
// A nonce is required
|
||||
return false;
|
||||
}
|
||||
} catch (TimeoutException e) {
|
||||
LOGGER.info(String.format("Timed out computing nonce for account %.8s", Base58.encode(publicKey)));
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] signature = Qortal25519Extras.signForAggregation(privateKey, timestampBytes);
|
||||
|
||||
// Our account is online
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
||||
ourOnlineAccounts.add(ourOnlineAccountData);
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, nonce);
|
||||
|
||||
// Make sure to verify before adding
|
||||
if (verifyMemoryPoW(ourOnlineAccountData, null)) {
|
||||
ourOnlineAccounts.add(ourOnlineAccountData);
|
||||
}
|
||||
}
|
||||
|
||||
this.hasOurOnlineAccounts = !ourOnlineAccounts.isEmpty();
|
||||
@@ -447,23 +601,72 @@ public class OnlineAccountsManager {
|
||||
boolean hasInfoChanged = addAccounts(ourOnlineAccounts);
|
||||
|
||||
if (!hasInfoChanged)
|
||||
return;
|
||||
return false;
|
||||
|
||||
Message messageV1 = new OnlineAccountsMessage(ourOnlineAccounts);
|
||||
Message messageV2 = new OnlineAccountsV2Message(ourOnlineAccounts);
|
||||
Message messageV3 = new OnlineAccountsV2Message(ourOnlineAccounts); // TODO: V3 message
|
||||
|
||||
Network.getInstance().broadcast(peer ->
|
||||
peer.getPeersVersion() >= ONLINE_ACCOUNTS_V3_PEER_VERSION
|
||||
? messageV3
|
||||
: peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION
|
||||
? messageV2
|
||||
: messageV1
|
||||
);
|
||||
Network.getInstance().broadcast(peer -> new OnlineAccountsV3Message(ourOnlineAccounts));
|
||||
|
||||
LOGGER.debug("Broadcasted {} online account{} with timestamp {}", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MemoryPoW
|
||||
|
||||
private byte[] getMemoryPoWBytes(byte[] publicKey, long onlineAccountsTimestamp) throws IOException {
|
||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
outputStream.write(publicKey);
|
||||
outputStream.write(timestampBytes);
|
||||
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private Integer computeMemoryPoW(byte[] bytes, byte[] publicKey, long onlineAccountsTimestamp) throws TimeoutException {
|
||||
LOGGER.info(String.format("Computing nonce for account %.8s and timestamp %d...", Base58.encode(publicKey), onlineAccountsTimestamp));
|
||||
|
||||
// Calculate the time until the next online timestamp and use it as a timeout when computing the nonce
|
||||
Long startTime = NTP.getTime();
|
||||
final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(startTime) + getOnlineTimestampModulus();
|
||||
long timeUntilNextTimestamp = nextOnlineAccountsTimestamp - startTime;
|
||||
|
||||
int difficulty = getPoWDifficulty(onlineAccountsTimestamp);
|
||||
Integer nonce = MemoryPoW.compute2(bytes, getPoWBufferSize(), difficulty, timeUntilNextTimestamp);
|
||||
|
||||
double totalSeconds = (NTP.getTime() - startTime) / 1000.0f;
|
||||
int minutes = (int) ((totalSeconds % 3600) / 60);
|
||||
int seconds = (int) (totalSeconds % 60);
|
||||
double hashRate = nonce / totalSeconds;
|
||||
|
||||
LOGGER.info(String.format("Computed nonce for timestamp %d and account %.8s: %d. Buffer size: %d. Difficulty: %d. " +
|
||||
"Time taken: %02d:%02d. Hashrate: %f", onlineAccountsTimestamp, Base58.encode(publicKey),
|
||||
nonce, getPoWBufferSize(), difficulty, minutes, seconds, hashRate));
|
||||
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public boolean verifyMemoryPoW(OnlineAccountData onlineAccountData, long[] workBuffer) {
|
||||
// Require a valid nonce value
|
||||
if (onlineAccountData.getNonce() == null || onlineAccountData.getNonce() < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int nonce = onlineAccountData.getNonce();
|
||||
|
||||
byte[] mempowBytes;
|
||||
try {
|
||||
mempowBytes = this.getMemoryPoWBytes(onlineAccountData.getPublicKey(), onlineAccountData.getTimestamp());
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the nonce
|
||||
return MemoryPoW.verify2(mempowBytes, workBuffer, getPoWBufferSize(), getPoWDifficulty(onlineAccountData.getTimestamp()), nonce);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether online accounts manager has any online accounts with timestamp recent enough to be considered currently online.
|
||||
*/
|
||||
@@ -498,7 +701,7 @@ public class OnlineAccountsManager {
|
||||
*/
|
||||
// Block::mint() - only wants online accounts with (online) timestamp that matches block's (online) timestamp so they can be added to new block
|
||||
public List<OnlineAccountData> getOnlineAccounts(long onlineTimestamp) {
|
||||
LOGGER.info(String.format("caller's timestamp: %d, our timestamps: %s", onlineTimestamp, String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
|
||||
LOGGER.debug(String.format("caller's timestamp: %d, our timestamps: %s", onlineTimestamp, String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
|
||||
|
||||
return new ArrayList<>(Set.copyOf(this.currentOnlineAccounts.getOrDefault(onlineTimestamp, Collections.emptySet())));
|
||||
}
|
||||
@@ -544,11 +747,12 @@ public class OnlineAccountsManager {
|
||||
* Typically called by {@link Block#areOnlineAccountsValid()}
|
||||
*/
|
||||
public void addBlocksOnlineAccounts(Set<OnlineAccountData> blocksOnlineAccounts, Long timestamp) {
|
||||
// We want to add to 'current' in preference if possible
|
||||
if (this.currentOnlineAccounts.containsKey(timestamp)) {
|
||||
addAccounts(blocksOnlineAccounts);
|
||||
// If these are current accounts, then there is no need to cache them, and should instead rely
|
||||
// on the more complete entries we already have in self.currentOnlineAccounts.
|
||||
// Note: since sig-agg, we no longer have individual signatures included in blocks, so we
|
||||
// mustn't add anything to currentOnlineAccounts from here.
|
||||
if (this.currentOnlineAccounts.containsKey(timestamp))
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to block cache instead
|
||||
this.latestBlocksOnlineAccounts.computeIfAbsent(timestamp, k -> ConcurrentHashMap.newKeySet())
|
||||
@@ -568,106 +772,6 @@ public class OnlineAccountsManager {
|
||||
|
||||
// Network handlers
|
||||
|
||||
public void onNetworkGetOnlineAccountsMessage(Peer peer, Message message) {
|
||||
GetOnlineAccountsMessage getOnlineAccountsMessage = (GetOnlineAccountsMessage) message;
|
||||
|
||||
List<OnlineAccountData> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
|
||||
|
||||
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
|
||||
List<OnlineAccountData> accountsToSend = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
|
||||
int prefilterSize = accountsToSend.size();
|
||||
|
||||
Iterator<OnlineAccountData> iterator = accountsToSend.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccountData onlineAccountData = iterator.next();
|
||||
|
||||
for (OnlineAccountData excludeAccountData : excludeAccounts) {
|
||||
if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (accountsToSend.isEmpty())
|
||||
return;
|
||||
|
||||
Message onlineAccountsMessage = new OnlineAccountsMessage(accountsToSend);
|
||||
peer.sendMessage(onlineAccountsMessage);
|
||||
|
||||
LOGGER.debug("Sent {} of our {} online accounts to {}", accountsToSend.size(), prefilterSize, peer);
|
||||
}
|
||||
|
||||
public void onNetworkOnlineAccountsMessage(Peer peer, Message message) {
|
||||
OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message;
|
||||
|
||||
List<OnlineAccountData> peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
LOGGER.debug("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
|
||||
|
||||
int importCount = 0;
|
||||
|
||||
// Add any online accounts to the queue that aren't already present
|
||||
for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
|
||||
boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
|
||||
|
||||
if (isNewEntry)
|
||||
importCount++;
|
||||
}
|
||||
|
||||
if (importCount > 0)
|
||||
LOGGER.debug("Added {} online accounts to queue", importCount);
|
||||
}
|
||||
|
||||
public void onNetworkGetOnlineAccountsV2Message(Peer peer, Message message) {
|
||||
GetOnlineAccountsV2Message getOnlineAccountsMessage = (GetOnlineAccountsV2Message) message;
|
||||
|
||||
List<OnlineAccountData> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
|
||||
|
||||
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
|
||||
List<OnlineAccountData> accountsToSend = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
|
||||
int prefilterSize = accountsToSend.size();
|
||||
|
||||
Iterator<OnlineAccountData> iterator = accountsToSend.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccountData onlineAccountData = iterator.next();
|
||||
|
||||
for (OnlineAccountData excludeAccountData : excludeAccounts) {
|
||||
if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (accountsToSend.isEmpty())
|
||||
return;
|
||||
|
||||
Message onlineAccountsMessage = new OnlineAccountsV2Message(accountsToSend);
|
||||
peer.sendMessage(onlineAccountsMessage);
|
||||
|
||||
LOGGER.debug("Sent {} of our {} online accounts to {}", accountsToSend.size(), prefilterSize, peer);
|
||||
}
|
||||
|
||||
public void onNetworkOnlineAccountsV2Message(Peer peer, Message message) {
|
||||
OnlineAccountsV2Message onlineAccountsMessage = (OnlineAccountsV2Message) message;
|
||||
|
||||
List<OnlineAccountData> peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
LOGGER.debug("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
|
||||
|
||||
int importCount = 0;
|
||||
|
||||
// Add any online accounts to the queue that aren't already present
|
||||
for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
|
||||
boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
|
||||
|
||||
if (isNewEntry)
|
||||
importCount++;
|
||||
}
|
||||
|
||||
if (importCount > 0)
|
||||
LOGGER.debug("Added {} online accounts to queue", importCount);
|
||||
}
|
||||
|
||||
public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) {
|
||||
GetOnlineAccountsV3Message getOnlineAccountsMessage = (GetOnlineAccountsV3Message) message;
|
||||
|
||||
@@ -688,7 +792,7 @@ public class OnlineAccountsManager {
|
||||
Set<OnlineAccountData> timestampsOnlineAccounts = this.currentOnlineAccounts.getOrDefault(timestamp, Collections.emptySet());
|
||||
outgoingOnlineAccounts.addAll(timestampsOnlineAccounts);
|
||||
|
||||
LOGGER.debug(() -> String.format("Going to send all %d online accounts for timestamp %d", timestampsOnlineAccounts.size(), timestamp));
|
||||
LOGGER.trace(() -> String.format("Going to send all %d online accounts for timestamp %d", timestampsOnlineAccounts.size(), timestamp));
|
||||
} else {
|
||||
// Quick cache of which leading bytes to send so we only have to filter once
|
||||
Set<Byte> outgoingLeadingBytes = new HashSet<>();
|
||||
@@ -712,7 +816,7 @@ public class OnlineAccountsManager {
|
||||
.forEach(outgoingOnlineAccounts::add);
|
||||
|
||||
if (outgoingOnlineAccounts.size() > beforeAddSize)
|
||||
LOGGER.debug(String.format("Going to send %d online accounts for timestamp %d and leading bytes %s",
|
||||
LOGGER.trace(String.format("Going to send %d online accounts for timestamp %d and leading bytes %s",
|
||||
outgoingOnlineAccounts.size() - beforeAddSize,
|
||||
timestamp,
|
||||
outgoingLeadingBytes.stream().sorted(Byte::compareUnsigned).map(leadingByte -> String.format("%02x", leadingByte)).collect(Collectors.joining(", "))
|
||||
@@ -721,9 +825,34 @@ public class OnlineAccountsManager {
|
||||
}
|
||||
}
|
||||
|
||||
Message onlineAccountsMessage = new OnlineAccountsV2Message(outgoingOnlineAccounts); // TODO: V3 message
|
||||
peer.sendMessage(onlineAccountsMessage);
|
||||
peer.sendMessage(new OnlineAccountsV3Message(outgoingOnlineAccounts));
|
||||
|
||||
LOGGER.debug("Sent {} online accounts to {}", outgoingOnlineAccounts.size(), peer);
|
||||
LOGGER.trace("Sent {} online accounts to {}", outgoingOnlineAccounts.size(), peer);
|
||||
}
|
||||
|
||||
public void onNetworkOnlineAccountsV3Message(Peer peer, Message message) {
|
||||
OnlineAccountsV3Message onlineAccountsMessage = (OnlineAccountsV3Message) message;
|
||||
|
||||
List<OnlineAccountData> peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
LOGGER.trace("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
|
||||
|
||||
int importCount = 0;
|
||||
|
||||
// Add any online accounts to the queue that aren't already present
|
||||
for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
|
||||
|
||||
Set<OnlineAccountData> onlineAccounts = this.currentOnlineAccounts.computeIfAbsent(onlineAccountData.getTimestamp(), k -> ConcurrentHashMap.newKeySet());
|
||||
if (onlineAccounts.contains(onlineAccountData))
|
||||
// We have already validated this online account
|
||||
continue;
|
||||
|
||||
boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
|
||||
|
||||
if (isNewEntry)
|
||||
importCount++;
|
||||
}
|
||||
|
||||
if (importCount > 0)
|
||||
LOGGER.debug("Added {} online accounts to queue", importCount);
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -99,14 +100,19 @@ public class PirateChainWalletController extends Thread {
|
||||
LOGGER.debug("Syncing Pirate Chain wallet...");
|
||||
String response = LiteWalletJni.execute("sync", "");
|
||||
LOGGER.debug("sync response: {}", response);
|
||||
JSONObject json = new JSONObject(response);
|
||||
if (json.has("result")) {
|
||||
String result = json.getString("result");
|
||||
|
||||
// We may have to set wallet to ready if this is the first ever successful sync
|
||||
if (Objects.equals(result, "success")) {
|
||||
this.currentWallet.setReady(true);
|
||||
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
|
||||
@@ -238,10 +244,10 @@ public class PirateChainWalletController extends Thread {
|
||||
if (osName.equals("Mac OS X") && osArchitecture.equals("x86_64")) {
|
||||
return "librust-macos-x86_64.dylib";
|
||||
}
|
||||
else if (osName.equals("Linux") && osArchitecture.equals("aarch64")) {
|
||||
else if ((osName.equals("Linux") || osName.equals("FreeBSD")) && osArchitecture.equals("aarch64")) {
|
||||
return "librust-linux-aarch64.so";
|
||||
}
|
||||
else if (osName.equals("Linux") && osArchitecture.equals("amd64")) {
|
||||
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")) {
|
||||
|
@@ -19,7 +19,6 @@ import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.CommonBlockData;
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
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 */
|
||||
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;
|
||||
@@ -76,6 +76,8 @@ public class Synchronizer extends Thread {
|
||||
private volatile boolean isSynchronizing = false;
|
||||
/** Temporary estimate of synchronization progress for SysTray use. */
|
||||
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 boolean syncRequestPending = false;
|
||||
@@ -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() {
|
||||
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
|
||||
peers.removeIf(Controller.hasInferiorChainTip);
|
||||
|
||||
// Disregard peers that have a block with an invalid signer
|
||||
peers.removeIf(Controller.hasInvalidSigner);
|
||||
|
||||
final int peersBeforeComparison = peers.size();
|
||||
|
||||
// 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();
|
||||
|
||||
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
|
||||
if (this.syncPercent < 100) {
|
||||
@@ -312,7 +329,7 @@ public class Synchronizer extends Thread {
|
||||
|
||||
case INFERIOR_CHAIN: {
|
||||
// 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))
|
||||
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()));
|
||||
|
||||
// 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");
|
||||
break;
|
||||
}
|
||||
@@ -341,7 +359,7 @@ public class Synchronizer extends Thread {
|
||||
// fall-through...
|
||||
case NOTHING_TO_DO: {
|
||||
// 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))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
@@ -369,8 +387,7 @@ public class Synchronizer extends Thread {
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
||||
Network.getInstance().broadcastOurChain();
|
||||
|
||||
EventBus.INSTANCE.notify(new NewChainTipEvent(priorChainTip, newChainTip));
|
||||
}
|
||||
@@ -397,9 +414,10 @@ public class Synchronizer extends Thread {
|
||||
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 (NTP.getTime() - timePeersLastAvailable > RECOVERY_MODE_TIMEOUT) {
|
||||
long recoveryModeTimeout = Settings.getInstance().getRecoveryModeTimeout();
|
||||
if (NTP.getTime() - timePeersLastAvailable > recoveryModeTimeout) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -513,13 +531,13 @@ public class Synchronizer extends Thread {
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
final int ourInitialHeight = ourLatestBlockData.getHeight();
|
||||
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
int peerHeight = peerChainTipData.getLastHeight();
|
||||
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
int peerHeight = peerChainTipData.getHeight();
|
||||
byte[] peersLastBlockSignature = peerChainTipData.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,
|
||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
||||
peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
|
||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
|
||||
|
||||
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
|
||||
@@ -637,9 +655,9 @@ public class Synchronizer extends Thread {
|
||||
return peers;
|
||||
|
||||
// Count the number of blocks this peer has beyond our common block
|
||||
final PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
final int peerHeight = peerChainTipData.getLastHeight();
|
||||
final byte[] peerLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
final int peerHeight = peerChainTipData.getHeight();
|
||||
final byte[] peerLastBlockSignature = peerChainTipData.getSignature();
|
||||
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
|
||||
int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
|
||||
@@ -727,8 +745,9 @@ public class Synchronizer extends Thread {
|
||||
|
||||
LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
|
||||
for (Peer peer : peersSharingCommonBlock) {
|
||||
final int peerHeight = peer.getChainTipData().getLastHeight();
|
||||
final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
|
||||
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
final int peerHeight = peerChainTipData.getHeight();
|
||||
final Long peerLastBlockTimestamp = peerChainTipData.getTimestamp();
|
||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||
final CommonBlockData peerCommonBlockData = peer.getCommonBlockData();
|
||||
|
||||
@@ -825,7 +844,7 @@ public class Synchronizer extends Thread {
|
||||
// Calculate the length of the shortest peer chain sharing this common block
|
||||
int minChainLength = 0;
|
||||
for (Peer peer : peersSharingCommonBlock) {
|
||||
final int peerHeight = peer.getChainTipData().getLastHeight();
|
||||
final int peerHeight = peer.getChainTipData().getHeight();
|
||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||
|
||||
if (peerAdditionalBlocksAfterCommonBlock < minChainLength || minChainLength == 0)
|
||||
@@ -933,13 +952,13 @@ public class Synchronizer extends Thread {
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
final int ourInitialHeight = ourLatestBlockData.getHeight();
|
||||
|
||||
PeerChainTipData peerChainTipData = peer.getChainTipData();
|
||||
int peerHeight = peerChainTipData.getLastHeight();
|
||||
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
|
||||
BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||
int peerHeight = peerChainTipData.getHeight();
|
||||
byte[] peersLastBlockSignature = peerChainTipData.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,
|
||||
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
|
||||
peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
|
||||
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp());
|
||||
LOGGER.info(syncString);
|
||||
|
||||
@@ -1102,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 (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
|
||||
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
||||
peer.setLastTooDivergentTime(NTP.getTime());
|
||||
return SynchronizationResult.TOO_DIVERGENT;
|
||||
}
|
||||
|
||||
@@ -1111,6 +1131,9 @@ public class Synchronizer extends Thread {
|
||||
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
|
||||
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
|
||||
blockSummariesFromCommon.add(0, testBlockSummary);
|
||||
@@ -1246,7 +1269,14 @@ public class Synchronizer extends Thread {
|
||||
int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size();
|
||||
|
||||
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())
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
|
||||
@@ -1313,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)
|
||||
if (!recoveryMode && peer.getChainTipData() != null) {
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
|
||||
final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
|
||||
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {
|
||||
LOGGER.info(String.format("Peer %s is out of date, so abandoning sync attempt", peer));
|
||||
return SynchronizationResult.CHAIN_TIP_TOO_OLD;
|
||||
@@ -1448,6 +1478,12 @@ public class Synchronizer extends Thread {
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
synchronized (this.syncLock) {
|
||||
if (peer.getChainTipData() != null) {
|
||||
this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
}
|
||||
|
||||
@@ -1543,6 +1579,12 @@ public class Synchronizer extends Thread {
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
synchronized (this.syncLock) {
|
||||
if (peer.getChainTipData() != null) {
|
||||
this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
}
|
||||
|
||||
@@ -1553,12 +1595,19 @@ public class Synchronizer extends Thread {
|
||||
Message getBlockSummariesMessage = new GetBlockSummariesMessage(parentSignature, numberRequested);
|
||||
|
||||
Message message = peer.getResponse(getBlockSummariesMessage);
|
||||
if (message == null || message.getType() != MessageType.BLOCK_SUMMARIES)
|
||||
if (message == 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 {
|
||||
@@ -1577,8 +1626,20 @@ public class Synchronizer extends Thread {
|
||||
Message getBlockMessage = new GetBlockMessage(signature);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
case BLOCK: {
|
||||
|
@@ -82,7 +82,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
try {
|
||||
// Use a fixed thread pool to execute the arbitrary data file requests
|
||||
int threadCount = 10;
|
||||
int threadCount = 5;
|
||||
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
||||
@@ -288,7 +288,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
// The ID needs to match that of the original request
|
||||
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);
|
||||
requestingPeer.disconnect("failed to forward arbitrary data file");
|
||||
}
|
||||
@@ -564,13 +564,16 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
LOGGER.trace("Hash {} exists", hash58);
|
||||
|
||||
// We can serve the file directly as we already have it
|
||||
LOGGER.debug("Sending file {}...", arbitraryDataFile);
|
||||
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
|
||||
arbitraryDataFileMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(arbitraryDataFileMessage)) {
|
||||
LOGGER.debug("Couldn't sent file");
|
||||
if (!peer.sendMessageWithTimeout(arbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||
LOGGER.debug("Couldn't send file {}", arbitraryDataFile);
|
||||
peer.disconnect("failed to send file");
|
||||
}
|
||||
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
||||
else {
|
||||
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
||||
}
|
||||
}
|
||||
else if (relayInfo != null) {
|
||||
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
|
||||
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
|
||||
// TODO: use a different message type here
|
||||
Message fileUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||
// Send generic 'unknown' message as it's very short
|
||||
Message fileUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
|
||||
? new GenericUnknownMessage()
|
||||
: new BlockSummariesMessage(Collections.emptyList());
|
||||
fileUnknownMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(fileUnknownMessage)) {
|
||||
LOGGER.debug("Couldn't sent file-unknown response");
|
||||
|
@@ -42,6 +42,7 @@ public class AtStatesPruner implements Runnable {
|
||||
|
||||
repository.discardChanges();
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
repository.saveChanges();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
@@ -29,6 +29,7 @@ public class AtStatesTrimmer implements Runnable {
|
||||
|
||||
repository.discardChanges();
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
repository.saveChanges();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
@@ -16,7 +16,7 @@ public class BlockArchiver implements Runnable {
|
||||
|
||||
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() {
|
||||
Thread.currentThread().setName("Block archiver");
|
||||
|
@@ -1,884 +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 DogecoinACCTv2TradeBot implements AcctTradeBot {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2TradeBot.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 DogecoinACCTv2TradeBot 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 DogecoinACCTv2TradeBot() {
|
||||
}
|
||||
|
||||
public static synchronized DogecoinACCTv2TradeBot getInstance() {
|
||||
if (instance == null)
|
||||
instance = new DogecoinACCTv2TradeBot();
|
||||
|
||||
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 DOGE.
|
||||
* <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 Dogecoin) 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'/Dogecoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||
* <li>QORT amount on offer by Bob</li>
|
||||
* <li>DOGE 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 Dogecoin receiving address into public key hash (we only support P2PKH at this time)
|
||||
Address dogecoinReceivingAddress;
|
||||
try {
|
||||
dogecoinReceivingAddress = Address.fromString(Dogecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
||||
} 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();
|
||||
|
||||
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/DOGE ACCT";
|
||||
String description = "QORT/DOGE cross-chain trade";
|
||||
String aTType = "ACCT";
|
||||
String tags = "ACCT QORT DOGE";
|
||||
byte[] creationBytes = DogecoinACCTv2.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, DogecoinACCTv2.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.DOGECOIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
tradeBotCreateRequest.foreignAmount, null, null, null, dogecoinReceivingAccountInfo);
|
||||
|
||||
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 DOGE to an existing offer.
|
||||
* <p>
|
||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||
* and access to a Dogecoin 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 Dogecoin 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 Dogecoin main-net)
|
||||
* or 'tprv' for (Dogecoin 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 Dogecoin amount expected by 'Bob'.
|
||||
* <p>
|
||||
* If the Dogecoin 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 Dogecoin 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, DogecoinACCTv2.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.DOGECOIN.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 = Dogecoin.getInstance().getP2shFee(now);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.debug("Couldn't estimate Dogecoin 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 = Dogecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||
|
||||
// Build transaction for funding P2SH-A
|
||||
Transaction p2shFundingTransaction = Dogecoin.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 {
|
||||
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
|
||||
byte[] messageData = DogecoinACCTv2.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:
|
||||
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 = DogecoinACCTv2.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 Dogecoin 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;
|
||||
}
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.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 Dogecoin pubkeyhash and lockTime-A
|
||||
byte[] messageData = messageTransactionData.getData();
|
||||
DogecoinACCTv2.OfferMessageData offerMessageData = DogecoinACCTv2.extractOfferMessageData(messageData);
|
||||
if (offerMessageData == null)
|
||||
continue;
|
||||
|
||||
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDogecoinPKH;
|
||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||
int refundTimeout = DogecoinACCTv2.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||
|
||||
// Determine P2SH-A address and confirm funded
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.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 = DogecoinACCTv2.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 DOGE 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;
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.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 = dogecoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.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 = DogecoinACCTv2.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 = DogecoinACCTv2.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 DOGE 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 DOGE funds from P2SH-A
|
||||
* to Bob's 'foreign'/Dogecoin trade legacy-format address, as derived from trade private key.
|
||||
* <p>
|
||||
* (This could potentially be 'improved' to send DOGE 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 DOGE
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] secretA = DogecoinACCTv2.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
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.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 = dogecoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
|
||||
dogecoin.broadcastTransaction(p2shRedeemTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String receivingAddress = dogecoin.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;
|
||||
|
||||
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = dogecoin.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTimeA)
|
||||
return;
|
||||
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.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 = dogecoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
Address receiving = Address.fromString(dogecoin.getNetworkParameters(), receiveAddress);
|
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(dogecoin.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
||||
|
||||
dogecoin.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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,885 +0,0 @@
|
||||
package org.qortal.controller.tradebot;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
/**
|
||||
* Performing cross-chain trading steps on behalf of user.
|
||||
* <p>
|
||||
* We deal with three different independent state-spaces here:
|
||||
* <ul>
|
||||
* <li>Qortal blockchain</li>
|
||||
* <li>Foreign blockchain</li>
|
||||
* <li>Trade-bot entries</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class LitecoinACCTv2TradeBot implements AcctTradeBot {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(LitecoinACCTv2TradeBot.class);
|
||||
|
||||
public enum State implements TradeBot.StateNameAndValueSupplier {
|
||||
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
|
||||
BOB_WAITING_FOR_MESSAGE(15, true, true),
|
||||
BOB_WAITING_FOR_AT_REDEEM(25, true, true),
|
||||
BOB_DONE(30, false, false),
|
||||
BOB_REFUNDED(35, false, false),
|
||||
|
||||
ALICE_WAITING_FOR_AT_LOCK(85, true, true),
|
||||
ALICE_DONE(95, false, false),
|
||||
ALICE_REFUNDING_A(105, true, true),
|
||||
ALICE_REFUNDED(110, false, false);
|
||||
|
||||
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
public final int value;
|
||||
public final boolean requiresAtData;
|
||||
public final boolean requiresTradeData;
|
||||
|
||||
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||
this.value = value;
|
||||
this.requiresAtData = requiresAtData;
|
||||
this.requiresTradeData = requiresTradeData;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStateValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||
|
||||
private static LitecoinACCTv2TradeBot instance;
|
||||
|
||||
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
|
||||
.map(State::name)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
|
||||
private LitecoinACCTv2TradeBot() {
|
||||
}
|
||||
|
||||
public static synchronized LitecoinACCTv2TradeBot getInstance() {
|
||||
if (instance == null)
|
||||
instance = new LitecoinACCTv2TradeBot();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getEndStates() {
|
||||
return this.endStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for LTC.
|
||||
* <p>
|
||||
* Generates:
|
||||
* <ul>
|
||||
* <li>new 'trade' private key</li>
|
||||
* </ul>
|
||||
* Derives:
|
||||
* <ul>
|
||||
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
|
||||
* <li>'foreign' (as in Litecoin) public key, public key hash</li>
|
||||
* </ul>
|
||||
* A Qortal AT is then constructed including the following as constants in the 'data segment':
|
||||
* <ul>
|
||||
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
|
||||
* <li>'foreign'/Litecoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
|
||||
* <li>QORT amount on offer by Bob</li>
|
||||
* <li>LTC amount expected in return by Bob (from Alice)</li>
|
||||
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
|
||||
* </ul>
|
||||
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
|
||||
* <p>
|
||||
* Trade-bot will wait for Bob's AT to be deployed before taking next step.
|
||||
* <p>
|
||||
* @param repository
|
||||
* @param tradeBotCreateRequest
|
||||
* @return raw, unsigned DEPLOY_AT transaction
|
||||
* @throws DataException
|
||||
*/
|
||||
public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
|
||||
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||
|
||||
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
|
||||
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
|
||||
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
|
||||
|
||||
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||
|
||||
// Convert Litecoin receiving address into public key hash (we only support P2PKH at this time)
|
||||
Address litecoinReceivingAddress;
|
||||
try {
|
||||
litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
|
||||
} catch (AddressFormatException e) {
|
||||
throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
}
|
||||
if (litecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
|
||||
|
||||
byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.getHash();
|
||||
|
||||
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
|
||||
|
||||
// Deploy AT
|
||||
long timestamp = NTP.getTime();
|
||||
byte[] reference = creator.getLastReference();
|
||||
long fee = 0L;
|
||||
byte[] signature = null;
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
|
||||
|
||||
String name = "QORT/LTC ACCT";
|
||||
String description = "QORT/LTC cross-chain trade";
|
||||
String aTType = "ACCT";
|
||||
String tags = "ACCT QORT LTC";
|
||||
byte[] creationBytes = LitecoinACCTv2.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
|
||||
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
|
||||
long amount = tradeBotCreateRequest.fundingQortAmount;
|
||||
|
||||
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
|
||||
String atAddress = deployAtTransactionData.getAtAddress();
|
||||
|
||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, LitecoinACCTv2.NAME,
|
||||
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
|
||||
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
|
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||
null, null,
|
||||
SupportedBlockchain.LITECOIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
tradeBotCreateRequest.foreignAmount, null, null, null, litecoinReceivingAccountInfo);
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
|
||||
|
||||
// Attempt to backup the trade bot data
|
||||
TradeBot.backupTradeBotData(repository, null);
|
||||
|
||||
// Return to user for signing and broadcast as we don't have their Qortal private key
|
||||
try {
|
||||
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||
} catch (TransformationException e) {
|
||||
throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching LTC to an existing offer.
|
||||
* <p>
|
||||
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
|
||||
* and access to a Litecoin wallet via <tt>xprv58</tt>.
|
||||
* <p>
|
||||
* The <tt>crossChainTradeData</tt> contains the current trade offer state
|
||||
* as extracted from the AT's data segment.
|
||||
* <p>
|
||||
* Access to a funded wallet is via a Litecoin BIP32 hierarchical deterministic key,
|
||||
* passed via <tt>xprv58</tt>.
|
||||
* <b>This key will be stored in your node's database</b>
|
||||
* to allow trade-bot to create/fund the necessary P2SH transactions!
|
||||
* However, due to the nature of BIP32 keys, it is possible to give the trade-bot
|
||||
* only a subset of wallet access (see BIP32 for more details).
|
||||
* <p>
|
||||
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
|
||||
* Electrum wallet by going to the console tab and entering:<br>
|
||||
* <tt>wallet.keystore.xprv</tt><br>
|
||||
* which should result in a base58 string starting with either 'xprv' (for Litecoin main-net)
|
||||
* or 'tprv' for (Litecoin test-net).
|
||||
* <p>
|
||||
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
|
||||
* <p>
|
||||
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
|
||||
* with the Litecoin amount expected by 'Bob'.
|
||||
* <p>
|
||||
* If the Litecoin transaction is successfully broadcast to the network then
|
||||
* we also send a MESSAGE to Bob's trade-bot to let them know.
|
||||
* <p>
|
||||
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
|
||||
* <p>
|
||||
* @param repository
|
||||
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
|
||||
* @param xprv58 funded wallet xprv in base58
|
||||
* @return true if P2SH-A funding transaction successfully broadcast to Litecoin network, false otherwise
|
||||
* @throws DataException
|
||||
*/
|
||||
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
|
||||
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
|
||||
byte[] secretA = TradeBot.generateSecret();
|
||||
byte[] hashOfSecretA = Crypto.hash160(secretA);
|
||||
|
||||
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
|
||||
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
|
||||
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
|
||||
|
||||
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
|
||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
|
||||
|
||||
// We need to generate lockTime-A: add tradeTimeout to now
|
||||
long now = NTP.getTime();
|
||||
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
|
||||
|
||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, LitecoinACCTv2.NAME,
|
||||
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
|
||||
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
|
||||
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
|
||||
secretA, hashOfSecretA,
|
||||
SupportedBlockchain.LITECOIN.name(),
|
||||
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
|
||||
|
||||
// Attempt to backup the trade bot data
|
||||
// Include tradeBotData as an additional parameter, since it's not in the repository yet
|
||||
TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData));
|
||||
|
||||
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
|
||||
long p2shFee;
|
||||
try {
|
||||
p2shFee = Litecoin.getInstance().getP2shFee(now);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.debug("Couldn't estimate Litecoin fees?");
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
// Do not include fee for funding transaction as this is covered by buildSpend()
|
||||
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
|
||||
|
||||
// P2SH-A to be funded
|
||||
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
|
||||
String p2shAddress = Litecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
|
||||
|
||||
// Build transaction for funding P2SH-A
|
||||
Transaction p2shFundingTransaction = Litecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
|
||||
if (p2shFundingTransaction == null) {
|
||||
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
|
||||
return ResponseResult.BALANCE_ISSUE;
|
||||
}
|
||||
|
||||
try {
|
||||
Litecoin.getInstance().broadcastTransaction(p2shFundingTransaction);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
// We couldn't fund P2SH-A at this time
|
||||
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = LitecoinACCTv2.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
return ResponseResult.NETWORK_ISSUE;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||
|
||||
return ResponseResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||
if (tradeBotState == null)
|
||||
return true;
|
||||
|
||||
// If the AT doesn't exist then we might as well let the user tidy up
|
||||
if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
|
||||
return true;
|
||||
|
||||
switch (tradeBotState) {
|
||||
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||
case ALICE_DONE:
|
||||
case BOB_DONE:
|
||||
case ALICE_REFUNDED:
|
||||
case BOB_REFUNDED:
|
||||
case ALICE_REFUNDING_A:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
|
||||
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
|
||||
if (tradeBotState == null) {
|
||||
LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
ATData atData = null;
|
||||
CrossChainTradeData tradeData = null;
|
||||
|
||||
if (tradeBotState.requiresAtData) {
|
||||
// Attempt to fetch AT data
|
||||
atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
|
||||
if (atData == null) {
|
||||
LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tradeBotState.requiresTradeData) {
|
||||
tradeData = LitecoinACCTv2.getInstance().populateTradeData(repository, atData);
|
||||
if (tradeData == null) {
|
||||
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (tradeBotState) {
|
||||
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||
handleBobWaitingForAtConfirm(repository, tradeBotData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_MESSAGE:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_WAITING_FOR_AT_LOCK:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_AT_REDEEM:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_DONE:
|
||||
case BOB_DONE:
|
||||
break;
|
||||
|
||||
case ALICE_REFUNDING_A:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
|
||||
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_REFUNDED:
|
||||
case BOB_REFUNDED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is waiting for Bob's AT to deploy.
|
||||
* <p>
|
||||
* If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
|
||||
*/
|
||||
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
|
||||
if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
|
||||
return;
|
||||
|
||||
// We've waited ages for AT to be confirmed into a block but something has gone awry.
|
||||
// After this long we assume transaction loss so give up with trade-bot entry too.
|
||||
tradeBotData.setState(State.BOB_REFUNDED.name());
|
||||
tradeBotData.setStateValue(State.BOB_REFUNDED.value);
|
||||
tradeBotData.setTimestamp(NTP.getTime());
|
||||
// We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
|
||||
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
|
||||
TradeBot.notifyStateChange(tradeBotData);
|
||||
return;
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
|
||||
() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
|
||||
* <p>
|
||||
* It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
|
||||
* in which case trade-bot is done with this specific trade and finalizes on refunded state.
|
||||
* <p>
|
||||
* Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
|
||||
* <p>
|
||||
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
|
||||
* <p>
|
||||
* Assuming P2SH-A has at least expected Litecoin balance,
|
||||
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
|
||||
* <p>
|
||||
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
|
||||
* <p>
|
||||
* Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
|
||||
* extract secret-A needed to redeem Alice's P2SH.
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
// If AT has finished then Bob likely cancelled his trade offer
|
||||
if (atData.getIsFinished()) {
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||
() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
String address = tradeBotData.getTradeNativeAddress();
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
|
||||
|
||||
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
|
||||
if (messageTransactionData.isText())
|
||||
continue;
|
||||
|
||||
// We're expecting: HASH160(secret-A), Alice's Litecoin pubkeyhash and lockTime-A
|
||||
byte[] messageData = messageTransactionData.getData();
|
||||
LitecoinACCTv2.OfferMessageData offerMessageData = LitecoinACCTv2.extractOfferMessageData(messageData);
|
||||
if (offerMessageData == null)
|
||||
continue;
|
||||
|
||||
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerLitecoinPKH;
|
||||
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
|
||||
int lockTimeA = (int) offerMessageData.lockTimeA;
|
||||
long messageTimestamp = messageTransactionData.getTimestamp();
|
||||
int refundTimeout = LitecoinACCTv2.calcRefundTimeout(messageTimestamp, lockTimeA);
|
||||
|
||||
// Determine P2SH-A address and confirm funded
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
|
||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// There might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||
continue;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
// We've already redeemed this?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
// This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
|
||||
continue;
|
||||
|
||||
case FUNDED:
|
||||
// Fall-through out of switch...
|
||||
break;
|
||||
}
|
||||
|
||||
// Good to go - send MESSAGE to AT
|
||||
|
||||
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
|
||||
|
||||
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
||||
byte[] outgoingMessageData = LitecoinACCTv2.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
|
||||
String messageRecipient = tradeBotData.getAtAddress();
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
|
||||
|
||||
outgoingMessageTransaction.computeNonce();
|
||||
outgoingMessageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
|
||||
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
|
||||
* <p>
|
||||
* It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
|
||||
* this process has taken so long that we've reached P2SH-A's locktime, or that someone else
|
||||
* has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
|
||||
* <p>
|
||||
* Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
|
||||
* <p>
|
||||
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
|
||||
* <p>
|
||||
* In revealing a valid secret-A, Bob can then redeem the LTC funds from P2SH-A.
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
|
||||
return;
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
|
||||
// Refund P2SH-A if we've passed lockTime-A
|
||||
if (NTP.getTime() >= lockTimeA * 1000L) {
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
case FUNDED:
|
||||
break;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
// Already redeemed?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> atData.getIsFinished()
|
||||
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
|
||||
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We're waiting for AT to be in TRADE mode
|
||||
if (crossChainTradeData.mode != AcctMode.TRADING)
|
||||
return;
|
||||
|
||||
// AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
|
||||
|
||||
// Find our MESSAGE to AT from previous state
|
||||
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
|
||||
crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
|
||||
if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
|
||||
LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
|
||||
int refundTimeout = LitecoinACCTv2.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
|
||||
|
||||
// Our calculated refundTimeout should match AT's refundTimeout
|
||||
if (refundTimeout != crossChainTradeData.refundTimeout) {
|
||||
LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
|
||||
// We'll eventually refund
|
||||
return;
|
||||
}
|
||||
|
||||
// We're good to redeem AT
|
||||
|
||||
// Send 'redeem' MESSAGE to AT using both secret
|
||||
byte[] secretA = tradeBotData.getSecret();
|
||||
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
|
||||
byte[] messageData = LitecoinACCTv2.buildRedeemMessage(secretA, qortalReceivingAddress);
|
||||
String messageRecipient = tradeBotData.getAtAddress();
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
if (!isMessageAlreadySent) {
|
||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// Reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||
() -> String.format("Redeeming AT %s. Funds should arrive at %s",
|
||||
tradeBotData.getAtAddress(), qortalReceivingAddress));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the LTC funds from P2SH-A.
|
||||
* <p>
|
||||
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
|
||||
* trade-bot is done with this specific trade and finalizes in refunded state.
|
||||
* <p>
|
||||
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the LTC funds from P2SH-A
|
||||
* to Bob's 'foreign'/Litecoin trade legacy-format address, as derived from trade private key.
|
||||
* <p>
|
||||
* (This could potentially be 'improved' to send LTC to any address of Bob's choosing by changing the transaction output).
|
||||
* <p>
|
||||
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
// AT should be 'finished' once Alice has redeemed QORT funds
|
||||
if (!atData.getIsFinished())
|
||||
// Not finished yet
|
||||
return;
|
||||
|
||||
// If AT is REFUNDED or CANCELLED then something has gone wrong
|
||||
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
|
||||
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the LTC
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
|
||||
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] secretA = LitecoinACCTv2.getInstance().findSecretA(repository, crossChainTradeData);
|
||||
if (secretA == null) {
|
||||
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use secret-A to redeem P2SH-A
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||
int lockTimeA = crossChainTradeData.lockTimeA;
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
|
||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
|
||||
return;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
// Double-check that we have redeemed P2SH-A...
|
||||
break;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
// Wait for AT to auto-refund
|
||||
return;
|
||||
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
|
||||
litecoin.broadcastTransaction(p2shRedeemTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String receivingAddress = litecoin.pkhToAddress(receivingAccountInfo);
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
|
||||
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trade-bot is attempting to refund P2SH-A.
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
int lockTimeA = tradeBotData.getLockTimeA();
|
||||
|
||||
// We can't refund P2SH-A until lockTime-A has passed
|
||||
if (NTP.getTime() <= lockTimeA * 1000L)
|
||||
return;
|
||||
|
||||
Litecoin litecoin = Litecoin.getInstance();
|
||||
|
||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||
int medianBlockTime = litecoin.getMedianBlockTime();
|
||||
if (medianBlockTime <= lockTimeA)
|
||||
return;
|
||||
|
||||
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
|
||||
|
||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
case FUNDING_IN_PROGRESS:
|
||||
// Still waiting for P2SH-A to be funded...
|
||||
return;
|
||||
|
||||
case REDEEM_IN_PROGRESS:
|
||||
case REDEEMED:
|
||||
// Too late!
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||
() -> String.format("P2SH-A %s already spent!", p2shAddressA));
|
||||
return;
|
||||
|
||||
case REFUND_IN_PROGRESS:
|
||||
case REFUNDED:
|
||||
break;
|
||||
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
|
||||
|
||||
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
|
||||
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
|
||||
|
||||
litecoin.broadcastTransaction(p2shRefundTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
|
||||
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
|
||||
* <p>
|
||||
* Will automatically update trade-bot state to <tt>ALICE_REFUNDING_A</tt> or <tt>ALICE_DONE</tt> as necessary.
|
||||
*
|
||||
* @throws DataException
|
||||
* @throws ForeignBlockchainException
|
||||
*/
|
||||
private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
|
||||
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
|
||||
// This is OK
|
||||
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
|
||||
return false;
|
||||
|
||||
boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
|
||||
|
||||
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
|
||||
if (isAtLockedToUs) {
|
||||
// AT is trading with us - OK
|
||||
return false;
|
||||
} else {
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
|
||||
// We've redeemed already?
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
|
||||
() -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
|
||||
} else {
|
||||
// Any other state is not good, so start defensive refund
|
||||
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
|
||||
() -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
|
||||
return (lockTimeA - tradeTimeout * 60) * 1000L;
|
||||
}
|
||||
|
||||
}
|
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
@@ -317,20 +318,27 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
|
||||
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);
|
||||
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||
// 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.sign(sender);
|
||||
messageTransaction.computeNonce();
|
||||
messageTransaction.sign(sender);
|
||||
|
||||
// reset repository state to prevent deadlock
|
||||
repository.discardChanges();
|
||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||
// reset repository state to prevent deadlock
|
||||
threadsRepository.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;
|
||||
}
|
||||
if (result != ValidationResult.OK) {
|
||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||
}
|
||||
} 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));
|
||||
|
@@ -523,7 +523,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
|
||||
|
||||
PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
@@ -613,7 +613,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
|
||||
PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
@@ -751,7 +751,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
String receivingAddress = Bech32.encode("zs", receivingAccountInfo);
|
||||
|
||||
PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
@@ -822,7 +822,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot {
|
||||
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
|
||||
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
|
||||
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
|
||||
PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
|
||||
|
||||
switch (htlcStatusA) {
|
||||
case UNFUNDED:
|
||||
|
@@ -96,10 +96,8 @@ public class TradeBot implements Listener {
|
||||
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(BitcoinACCTv3.class, BitcoinACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(LitecoinACCTv2.class, LitecoinACCTv2TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
|
||||
acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
|
||||
@@ -470,9 +468,6 @@ public class TradeBot implements Listener {
|
||||
|
||||
List<TradePresenceData> safeTradePresences = List.copyOf(this.safeAllTradePresencesByPubkey.values());
|
||||
|
||||
if (safeTradePresences.isEmpty())
|
||||
return;
|
||||
|
||||
LOGGER.debug("Broadcasting all {} known trade presences. Next broadcast timestamp: {}",
|
||||
safeTradePresences.size(), nextTradePresenceBroadcastTimestamp
|
||||
);
|
||||
@@ -639,7 +634,7 @@ public class TradeBot implements Listener {
|
||||
}
|
||||
|
||||
if (newCount > 0) {
|
||||
LOGGER.debug("New trade presences: {}", newCount);
|
||||
LOGGER.debug("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
|
||||
rebuildSafeAllTradePresences();
|
||||
}
|
||||
}
|
||||
|
@@ -134,6 +134,8 @@ public class Digibyte extends Bitcoiny {
|
||||
Context bitcoinjContext = new Context(digibyteNet.getParams());
|
||||
|
||||
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
electrumX.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
@@ -1,861 +0,0 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.ciyam.at.*;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.ciyam.at.OpCode.calcOffset;
|
||||
|
||||
/**
|
||||
* Cross-chain trade AT
|
||||
*
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Bob generates Dogecoin & 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 Dogecoin & Qortal 'trade' keys</li>
|
||||
* <li>Alice funds Dogecoin 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' Dogecoin 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 Dogecoin 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 Dogecoin trade key and secret-A</li>
|
||||
* <li>P2SH-A DOGE funds end up at Dogecoin address determined by redeem transaction output(s)</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public class DogecoinACCTv2 implements ACCT {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2.class);
|
||||
|
||||
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;
|
||||
|
||||
/** <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[] partnerDogecoinPKH;
|
||||
public byte[] hashOfSecretA;
|
||||
public long lockTimeA;
|
||||
}
|
||||
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDogecoinPKH*/ + 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 Dogecoin 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 DogecoinACCTv2 instance;
|
||||
|
||||
private DogecoinACCTv2() {
|
||||
}
|
||||
|
||||
public static synchronized DogecoinACCTv2 getInstance() {
|
||||
if (instance == null)
|
||||
instance = new DogecoinACCTv2();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getCodeBytesHash() {
|
||||
return CODE_BYTES_HASH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModeByteOffset() {
|
||||
return MODE_BYTE_OFFSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForeignBlockchain getBlockchain() {
|
||||
return Dogecoin.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 dogecoinPublicKeyHash 20-byte HASH160 of creator's trade Dogecoin public key
|
||||
* @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 tradeTimeout suggested timeout for entire trade
|
||||
*/
|
||||
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] dogecoinPublicKeyHash, long qortAmount, long dogecoinAmount, int tradeTimeout) {
|
||||
if (dogecoinPublicKeyHash.length != 20)
|
||||
throw new IllegalArgumentException("Dogecoin 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 addrDogecoinPublicKeyHash = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrQortAmount = addrCounter++;
|
||||
final int addrDogecoinAmount = 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 addrTradeMessagePartnerDogecoinPKHOffset = addrCounter++;
|
||||
final int addrPartnerDogecoinPKHPointer = 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 addrPartnerDogecoinPKH = 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));
|
||||
|
||||
// Dogecoin public key hash
|
||||
assert dataByteBuffer.position() == addrDogecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrDogecoinPublicKeyHash incorrect";
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(dogecoinPublicKeyHash, 32, 0));
|
||||
|
||||
// Redeem Qort amount
|
||||
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||
dataByteBuffer.putLong(qortAmount);
|
||||
|
||||
// Expected Dogecoin amount
|
||||
assert dataByteBuffer.position() == addrDogecoinAmount * MachineState.VALUE_SIZE : "addrDogecoinAmount incorrect";
|
||||
dataByteBuffer.putLong(dogecoinAmount);
|
||||
|
||||
// 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 Dogecoin PKH
|
||||
assert dataByteBuffer.position() == addrTradeMessagePartnerDogecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDogecoinPKHOffset incorrect";
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
// Index into data segment of partner's Dogecoin PKH, used by GET_B_IND
|
||||
assert dataByteBuffer.position() == addrPartnerDogecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerDogecoinPKHPointer incorrect";
|
||||
dataByteBuffer.putLong(addrPartnerDogecoinPKH);
|
||||
|
||||
// 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 */
|
||||
|
||||
/* 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
|
||||
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 Dogecoin 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, addrTradeMessagePartnerDogecoinPKHOffset));
|
||||
// Store partner's Dogecoin PKH (we only really use values from B1-B3)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDogecoinPKHPointer));
|
||||
// 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 DOGE-QORT ACCT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
assert Arrays.equals(Crypto.digest(codeBytes), DogecoinACCTv2.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.DOGECOIN.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 Dogecoin/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 DOGE 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 Dogecoin PKH
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 8);
|
||||
|
||||
// Skip pointer to partner's Dogecoin 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 Dogecoin PKH
|
||||
byte[] partnerDogecoinPKH = new byte[20];
|
||||
dataByteBuffer.get(partnerDogecoinPKH);
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDogecoinPKH.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 = partnerDogecoinPKH;
|
||||
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.partnerDogecoinPKH = 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;
|
||||
}
|
||||
|
||||
}
|
@@ -40,7 +40,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
||||
|
||||
private static final int RESPONSE_TIME_READINGS = 5;
|
||||
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
|
||||
private static final long MAX_AVG_RESPONSE_TIME = 1000L; // ms
|
||||
|
||||
public static class Server {
|
||||
String hostname;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -4,6 +4,12 @@ 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;
|
||||
@@ -352,6 +358,12 @@ public class PirateChain extends Bitcoiny {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
@@ -3,25 +3,17 @@ package org.qortal.crosschain;
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.bitcoinj.core.*;
|
||||
import org.bitcoinj.core.Transaction.SigHash;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.script.ScriptChunk;
|
||||
import org.bitcoinj.script.ScriptOpCodes;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import static org.qortal.crosschain.BitcoinyHTLC.Status;
|
||||
|
||||
public class PirateChainHTLC {
|
||||
|
||||
public enum Status {
|
||||
UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED
|
||||
}
|
||||
|
||||
public static final int SECRET_LENGTH = 32;
|
||||
public static final int MIN_LOCKTIME = 1500000000;
|
||||
|
||||
|
@@ -117,7 +117,7 @@ public class PirateWallet {
|
||||
// Restore existing wallet
|
||||
String response = LiteWalletJni.initfromb64(serverUri, params, wallet, saplingOutput64, saplingSpend64);
|
||||
if (response != null && !response.contains("\"initalized\":true")) {
|
||||
LOGGER.info("Unable to initialize Pirate Chain wallet: {}", response);
|
||||
LOGGER.info("Unable to initialize Pirate Chain wallet at {}: {}", serverUri, response);
|
||||
return false;
|
||||
}
|
||||
this.seedPhrase = inputSeedPhrase;
|
||||
|
@@ -138,6 +138,8 @@ public class Ravencoin extends Bitcoiny {
|
||||
Context bitcoinjContext = new Context(ravencoinNet.getParams());
|
||||
|
||||
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||
|
||||
electrumX.setBlockchain(instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
|
@@ -29,7 +29,6 @@ public enum SupportedBlockchain {
|
||||
|
||||
LITECOIN(Arrays.asList(
|
||||
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)
|
||||
)) {
|
||||
@Override
|
||||
@@ -45,7 +44,6 @@ public enum SupportedBlockchain {
|
||||
|
||||
DOGECOIN(Arrays.asList(
|
||||
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)
|
||||
)) {
|
||||
@Override
|
||||
|
@@ -1,10 +1,44 @@
|
||||
package org.qortal.crypto;
|
||||
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
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) {
|
||||
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
|
||||
byte[] hash = Crypto.digest(data);
|
||||
|
||||
@@ -33,6 +67,13 @@ public class MemoryPoW {
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
return -1;
|
||||
|
||||
if (timeout != null) {
|
||||
long now = NTP.getTime();
|
||||
if (now > startTime + timeout) {
|
||||
throw new TimeoutException("Timeout reached");
|
||||
}
|
||||
}
|
||||
|
||||
seed *= seedMultiplier; // per nonce
|
||||
|
||||
state[0] = longHash[0] ^ seed;
|
||||
@@ -58,6 +99,10 @@ public class MemoryPoW {
|
||||
}
|
||||
|
||||
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
|
||||
byte[] hash = Crypto.digest(data);
|
||||
|
||||
@@ -70,7 +115,10 @@ public class MemoryPoW {
|
||||
byteBuffer = null;
|
||||
|
||||
int longBufferLength = workBufferLength / 8;
|
||||
long[] workBuffer = new long[longBufferLength];
|
||||
|
||||
if (workBuffer == null)
|
||||
workBuffer = new long[longBufferLength];
|
||||
|
||||
long[] state = new long[4];
|
||||
|
||||
long seed = 8682522807148012L;
|
||||
|
@@ -18,6 +18,7 @@ public class AccountData {
|
||||
protected int level;
|
||||
protected int blocksMinted;
|
||||
protected int blocksMintedAdjustment;
|
||||
protected int blocksMintedPenalty;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -25,7 +26,7 @@ public class 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.reference = reference;
|
||||
this.publicKey = publicKey;
|
||||
@@ -34,10 +35,11 @@ public class AccountData {
|
||||
this.level = level;
|
||||
this.blocksMinted = blocksMinted;
|
||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -102,6 +104,14 @@ public class AccountData {
|
||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||
}
|
||||
|
||||
public int getBlocksMintedPenalty() {
|
||||
return this.blocksMintedPenalty;
|
||||
}
|
||||
|
||||
public void setBlocksMintedPenalty(int blocksMintedPenalty) {
|
||||
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||
}
|
||||
|
||||
// Comparison
|
||||
|
||||
@Override
|
||||
|
@@ -0,0 +1,52 @@
|
||||
package org.qortal.data.account;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AccountPenaltyData {
|
||||
|
||||
// Properties
|
||||
private String address;
|
||||
private int blocksMintedPenalty;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAXB
|
||||
protected AccountPenaltyData() {
|
||||
}
|
||||
|
||||
public AccountPenaltyData(String address, int blocksMintedPenalty) {
|
||||
this.address = address;
|
||||
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
|
||||
public String getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public int getBlocksMintedPenalty() {
|
||||
return this.blocksMintedPenalty;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s has penalty %d", this.address, this.blocksMintedPenalty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object b) {
|
||||
if (!(b instanceof AccountPenaltyData))
|
||||
return false;
|
||||
|
||||
return this.getAddress().equals(((AccountPenaltyData) b).getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address.hashCode();
|
||||
}
|
||||
|
||||
}
|
@@ -24,7 +24,10 @@ public class ArbitraryResourceMetadata {
|
||||
this.description = description;
|
||||
this.tags = tags;
|
||||
this.category = category;
|
||||
this.categoryName = category.getName();
|
||||
|
||||
if (category != null) {
|
||||
this.categoryName = category.getName();
|
||||
}
|
||||
}
|
||||
|
||||
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata) {
|
||||
|
@@ -211,6 +211,14 @@ public class BlockData implements Serializable {
|
||||
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() {
|
||||
long onlineAccountSignaturesTrimmedTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
|
||||
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||
|
@@ -11,11 +11,12 @@ public class BlockSummaryData {
|
||||
private int height;
|
||||
private byte[] signature;
|
||||
private byte[] minterPublicKey;
|
||||
private int onlineAccountsCount;
|
||||
|
||||
// Optional, set during construction
|
||||
private Integer onlineAccountsCount;
|
||||
private Long timestamp;
|
||||
private Integer transactionCount;
|
||||
private byte[] reference;
|
||||
|
||||
// Optional, set after construction
|
||||
private Integer minterLevel;
|
||||
@@ -25,6 +26,15 @@ public class 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) {
|
||||
this.height = height;
|
||||
this.signature = signature;
|
||||
@@ -32,13 +42,16 @@ public class BlockSummaryData {
|
||||
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.signature = signature;
|
||||
this.minterPublicKey = minterPublicKey;
|
||||
this.onlineAccountsCount = onlineAccountsCount;
|
||||
this.timestamp = timestamp;
|
||||
this.transactionCount = transactionCount;
|
||||
this.reference = reference;
|
||||
}
|
||||
|
||||
public BlockSummaryData(BlockData blockData) {
|
||||
@@ -49,6 +62,7 @@ public class BlockSummaryData {
|
||||
|
||||
this.timestamp = blockData.getTimestamp();
|
||||
this.transactionCount = blockData.getTransactionCount();
|
||||
this.reference = blockData.getReference();
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -65,7 +79,7 @@ public class BlockSummaryData {
|
||||
return this.minterPublicKey;
|
||||
}
|
||||
|
||||
public int getOnlineAccountsCount() {
|
||||
public Integer getOnlineAccountsCount() {
|
||||
return this.onlineAccountsCount;
|
||||
}
|
||||
|
||||
@@ -77,6 +91,10 @@ public class BlockSummaryData {
|
||||
return this.transactionCount;
|
||||
}
|
||||
|
||||
public byte[] getReference() {
|
||||
return this.reference;
|
||||
}
|
||||
|
||||
public Integer getMinterLevel() {
|
||||
return this.minterLevel;
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
package org.qortal.data.block;
|
||||
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.math.BigInteger;
|
||||
@@ -14,14 +12,14 @@ public class CommonBlockData {
|
||||
private BlockSummaryData commonBlockSummary = null;
|
||||
private List<BlockSummaryData> blockSummariesAfterCommonBlock = null;
|
||||
private BigInteger chainWeight = null;
|
||||
private PeerChainTipData chainTipData = null;
|
||||
private BlockSummaryData chainTipData = null;
|
||||
|
||||
// Constructors
|
||||
|
||||
protected CommonBlockData() {
|
||||
}
|
||||
|
||||
public CommonBlockData(BlockSummaryData commonBlockSummary, PeerChainTipData chainTipData) {
|
||||
public CommonBlockData(BlockSummaryData commonBlockSummary, BlockSummaryData chainTipData) {
|
||||
this.commonBlockSummary = commonBlockSummary;
|
||||
this.chainTipData = chainTipData;
|
||||
}
|
||||
@@ -49,7 +47,7 @@ public class CommonBlockData {
|
||||
this.chainWeight = chainWeight;
|
||||
}
|
||||
|
||||
public PeerChainTipData getChainTipData() {
|
||||
public BlockSummaryData getChainTipData() {
|
||||
return this.chainTipData;
|
||||
}
|
||||
|
||||
|
@@ -27,6 +27,8 @@ public class ChatMessage {
|
||||
|
||||
private String recipientName;
|
||||
|
||||
private byte[] chatReference;
|
||||
|
||||
private byte[] data;
|
||||
|
||||
private boolean isText;
|
||||
@@ -42,8 +44,8 @@ public class ChatMessage {
|
||||
|
||||
// For repository use
|
||||
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
|
||||
String senderName, String recipient, String recipientName, byte[] data, boolean isText,
|
||||
boolean isEncrypted, byte[] signature) {
|
||||
String senderName, String recipient, String recipientName, byte[] chatReference, byte[] data,
|
||||
boolean isText, boolean isEncrypted, byte[] signature) {
|
||||
this.timestamp = timestamp;
|
||||
this.txGroupId = txGroupId;
|
||||
this.reference = reference;
|
||||
@@ -52,6 +54,7 @@ public class ChatMessage {
|
||||
this.senderName = senderName;
|
||||
this.recipient = recipient;
|
||||
this.recipientName = recipientName;
|
||||
this.chatReference = chatReference;
|
||||
this.data = data;
|
||||
this.isText = isText;
|
||||
this.isEncrypted = isEncrypted;
|
||||
@@ -90,6 +93,10 @@ public class ChatMessage {
|
||||
return this.recipientName;
|
||||
}
|
||||
|
||||
public byte[] getChatReference() {
|
||||
return this.chatReference;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ public class OnlineAccountData {
|
||||
protected long timestamp;
|
||||
protected byte[] signature;
|
||||
protected byte[] publicKey;
|
||||
protected Integer nonce;
|
||||
|
||||
@XmlTransient
|
||||
private int hash;
|
||||
@@ -26,10 +27,15 @@ public class 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.signature = signature;
|
||||
this.publicKey = publicKey;
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
|
||||
this(timestamp, signature, publicKey, null);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
@@ -44,6 +50,10 @@ public class OnlineAccountData {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
public Integer getNonce() {
|
||||
return this.nonce;
|
||||
}
|
||||
|
||||
// For JAXB
|
||||
@XmlElement(name = "address")
|
||||
protected String getAddress() {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -28,6 +28,9 @@ public class PeerData {
|
||||
private Long addedWhen;
|
||||
private String addedBy;
|
||||
|
||||
/** The number of consecutive times we failed to sync with this peer */
|
||||
private int failedSyncCount = 0;
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAXB serialization
|
||||
@@ -92,6 +95,18 @@ public class PeerData {
|
||||
return this.addedBy;
|
||||
}
|
||||
|
||||
public int getFailedSyncCount() {
|
||||
return this.failedSyncCount;
|
||||
}
|
||||
|
||||
public void setFailedSyncCount(int failedSyncCount) {
|
||||
this.failedSyncCount = failedSyncCount;
|
||||
}
|
||||
|
||||
public void incrementFailedSyncCount() {
|
||||
this.failedSyncCount++;
|
||||
}
|
||||
|
||||
// Pretty peerAddress getter for JAXB
|
||||
@XmlElement(name = "address")
|
||||
protected String getPrettyAddress() {
|
||||
|
@@ -26,6 +26,8 @@ public class ChatTransactionData extends TransactionData {
|
||||
|
||||
private String recipient; // can be null
|
||||
|
||||
private byte[] chatReference; // can be null
|
||||
|
||||
@Schema(description = "raw message data, possibly UTF8 text", example = "2yGEbwRFyhPZZckKA")
|
||||
private byte[] data;
|
||||
|
||||
@@ -44,13 +46,14 @@ public class ChatTransactionData extends TransactionData {
|
||||
}
|
||||
|
||||
public ChatTransactionData(BaseTransactionData baseTransactionData,
|
||||
String sender, int nonce, String recipient, byte[] data, boolean isText, boolean isEncrypted) {
|
||||
String sender, int nonce, String recipient, byte[] chatReference, byte[] data, boolean isText, boolean isEncrypted) {
|
||||
super(TransactionType.CHAT, baseTransactionData);
|
||||
|
||||
this.senderPublicKey = baseTransactionData.creatorPublicKey;
|
||||
this.sender = sender;
|
||||
this.nonce = nonce;
|
||||
this.recipient = recipient;
|
||||
this.chatReference = chatReference;
|
||||
this.data = data;
|
||||
this.isText = isText;
|
||||
this.isEncrypted = isEncrypted;
|
||||
@@ -78,6 +81,14 @@ public class ChatTransactionData extends TransactionData {
|
||||
return this.recipient;
|
||||
}
|
||||
|
||||
public byte[] getChatReference() {
|
||||
return this.chatReference;
|
||||
}
|
||||
|
||||
public void setChatReference(byte[] chatReference) {
|
||||
this.chatReference = chatReference;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.data.transaction;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@@ -90,4 +91,17 @@ public class DeployAtTransactionData extends TransactionData {
|
||||
this.aTAddress = AtAddress;
|
||||
}
|
||||
|
||||
// Re-expose creatorPublicKey for this transaction type for JAXB
|
||||
@XmlElement(name = "creatorPublicKey")
|
||||
@Schema(name = "creatorPublicKey", description = "AT creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
public byte[] getAtCreatorPublicKey() {
|
||||
return this.creatorPublicKey;
|
||||
}
|
||||
|
||||
@XmlElement(name = "creatorPublicKey")
|
||||
@Schema(name = "creatorPublicKey", description = "AT creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
public void setAtCreatorPublicKey(byte[] creatorPublicKey) {
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -128,6 +128,10 @@ public abstract class TransactionData {
|
||||
return this.txGroupId;
|
||||
}
|
||||
|
||||
public void setTxGroupId(int txGroupId) {
|
||||
this.txGroupId = txGroupId;
|
||||
}
|
||||
|
||||
public byte[] getReference() {
|
||||
return this.reference;
|
||||
}
|
||||
|
@@ -80,6 +80,9 @@ public class Group {
|
||||
// Useful constants
|
||||
public static final int NO_GROUP = 0;
|
||||
|
||||
// Null owner address corresponds with public key "11111111111111111111111111111111"
|
||||
public static String NULL_OWNER_ADDRESS = "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG";
|
||||
|
||||
public static final int MIN_NAME_SIZE = 3;
|
||||
public static final int MAX_NAME_SIZE = 32;
|
||||
public static final int MAX_DESCRIPTION_SIZE = 128;
|
||||
|
@@ -265,7 +265,7 @@ public enum Handshake {
|
||||
private static final long PEER_VERSION_131 = 0x0100030001L;
|
||||
|
||||
/** Minimum peer version that we are allowed to communicate with */
|
||||
private static final String MIN_PEER_VERSION = "3.1.0";
|
||||
private static final String MIN_PEER_VERSION = "3.8.2";
|
||||
|
||||
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
||||
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
||||
|
@@ -11,6 +11,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.message.*;
|
||||
@@ -90,6 +91,8 @@ public class Network {
|
||||
|
||||
private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds
|
||||
|
||||
private static final int BROADCAST_CHAIN_TIP_DEPTH = 7; // Just enough to fill a SINGLE TCP packet (~1440 bytes)
|
||||
|
||||
// Generate our node keys / ID
|
||||
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
|
||||
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
|
||||
@@ -1087,10 +1090,16 @@ public class Network {
|
||||
|
||||
if (peer.isOutbound()) {
|
||||
if (!Settings.getInstance().isLite()) {
|
||||
// Send our height
|
||||
Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
|
||||
if (!peer.sendMessage(heightMessage)) {
|
||||
peer.disconnect("failed to send height/info");
|
||||
// Send our height / chain tip info
|
||||
Message message = this.buildHeightOrChainTipInfo(peer);
|
||||
|
||||
if (message == null) {
|
||||
peer.disconnect("Couldn't build our chain tip info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!peer.sendMessage(message)) {
|
||||
peer.disconnect("failed to send height / chain tip info");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1164,10 +1173,47 @@ public class Network {
|
||||
return new PeersV2Message(peerAddresses);
|
||||
}
|
||||
|
||||
public Message buildHeightMessage(Peer peer, BlockData blockData) {
|
||||
// HEIGHT_V2 contains way more useful info
|
||||
return new HeightV2Message(blockData.getHeight(), blockData.getSignature(),
|
||||
blockData.getTimestamp(), blockData.getMinterPublicKey());
|
||||
/** Builds either (legacy) HeightV2Message or (newer) BlockSummariesV2Message, depending on peer version.
|
||||
*
|
||||
* @return Message, or null if DataException was thrown.
|
||||
*/
|
||||
public Message buildHeightOrChainTipInfo(Peer peer) {
|
||||
if (peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION) {
|
||||
int latestHeight = Controller.getInstance().getChainHeight();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockSummaryData> latestBlockSummaries = repository.getBlockRepository().getBlockSummaries(latestHeight - BROADCAST_CHAIN_TIP_DEPTH, latestHeight);
|
||||
return new BlockSummariesV2Message(latestBlockSummaries);
|
||||
} catch (DataException e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// For older peers
|
||||
BlockData latestBlockData = Controller.getInstance().getChainTip();
|
||||
return new HeightV2Message(latestBlockData.getHeight(), latestBlockData.getSignature(),
|
||||
latestBlockData.getTimestamp(), latestBlockData.getMinterPublicKey());
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastOurChain() {
|
||||
BlockData latestBlockData = Controller.getInstance().getChainTip();
|
||||
int latestHeight = latestBlockData.getHeight();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<BlockSummaryData> latestBlockSummaries = repository.getBlockRepository().getBlockSummaries(latestHeight - BROADCAST_CHAIN_TIP_DEPTH, latestHeight);
|
||||
Message latestBlockSummariesMessage = new BlockSummariesV2Message(latestBlockSummaries);
|
||||
|
||||
// For older peers
|
||||
Message heightMessage = new HeightV2Message(latestBlockData.getHeight(), latestBlockData.getSignature(),
|
||||
latestBlockData.getTimestamp(), latestBlockData.getMinterPublicKey());
|
||||
|
||||
Network.getInstance().broadcast(broadcastPeer -> broadcastPeer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
|
||||
? latestBlockSummariesMessage
|
||||
: heightMessage
|
||||
);
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Couldn't broadcast our chain tip info", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Message buildNewTransactionMessage(Peer peer, TransactionData transactionData) {
|
||||
|
@@ -6,8 +6,8 @@ import com.google.common.net.InetAddresses;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.CommonBlockData;
|
||||
import org.qortal.data.network.PeerChainTipData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.network.message.ChallengeMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
@@ -148,13 +148,18 @@ public class Peer {
|
||||
/**
|
||||
* Latest block info as reported by peer.
|
||||
*/
|
||||
private PeerChainTipData peersChainTipData;
|
||||
private List<BlockSummaryData> peersChainTipData = Collections.emptyList();
|
||||
|
||||
/**
|
||||
* Our common block with this peer
|
||||
*/
|
||||
private CommonBlockData commonBlockData;
|
||||
|
||||
/**
|
||||
* Last time we detected this peer as TOO_DIVERGENT
|
||||
*/
|
||||
private Long lastTooDivergentTime;
|
||||
|
||||
// Message stats
|
||||
|
||||
private static class MessageStats {
|
||||
@@ -353,28 +358,42 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
public PeerChainTipData getChainTipData() {
|
||||
synchronized (this.peerInfoLock) {
|
||||
return this.peersChainTipData;
|
||||
}
|
||||
public BlockSummaryData getChainTipData() {
|
||||
List<BlockSummaryData> chainTipSummaries = this.peersChainTipData;
|
||||
|
||||
if (chainTipSummaries.isEmpty())
|
||||
return null;
|
||||
|
||||
// Return last entry, which should have greatest height
|
||||
return chainTipSummaries.get(chainTipSummaries.size() - 1);
|
||||
}
|
||||
|
||||
public void setChainTipData(PeerChainTipData chainTipData) {
|
||||
synchronized (this.peerInfoLock) {
|
||||
this.peersChainTipData = chainTipData;
|
||||
}
|
||||
public void setChainTipData(BlockSummaryData chainTipData) {
|
||||
this.peersChainTipData = Collections.singletonList(chainTipData);
|
||||
}
|
||||
|
||||
public List<BlockSummaryData> getChainTipSummaries() {
|
||||
return this.peersChainTipData;
|
||||
}
|
||||
|
||||
public void setChainTipSummaries(List<BlockSummaryData> chainTipSummaries) {
|
||||
this.peersChainTipData = List.copyOf(chainTipSummaries);
|
||||
}
|
||||
|
||||
public CommonBlockData getCommonBlockData() {
|
||||
synchronized (this.peerInfoLock) {
|
||||
return this.commonBlockData;
|
||||
}
|
||||
return this.commonBlockData;
|
||||
}
|
||||
|
||||
public void setCommonBlockData(CommonBlockData commonBlockData) {
|
||||
synchronized (this.peerInfoLock) {
|
||||
this.commonBlockData = commonBlockData;
|
||||
}
|
||||
this.commonBlockData = commonBlockData;
|
||||
}
|
||||
|
||||
public Long getLastTooDivergentTime() {
|
||||
return this.lastTooDivergentTime;
|
||||
}
|
||||
|
||||
public void setLastTooDivergentTime(Long lastTooDivergentTime) {
|
||||
this.lastTooDivergentTime = lastTooDivergentTime;
|
||||
}
|
||||
|
||||
public boolean isSyncInProgress() {
|
||||
@@ -904,20 +923,22 @@ public class Peer {
|
||||
// Common block data
|
||||
|
||||
public boolean canUseCachedCommonBlockData() {
|
||||
PeerChainTipData peerChainTipData = this.getChainTipData();
|
||||
CommonBlockData commonBlockData = this.getCommonBlockData();
|
||||
BlockSummaryData peerChainTipData = this.getChainTipData();
|
||||
if (peerChainTipData == null || peerChainTipData.getSignature() == null)
|
||||
return false;
|
||||
|
||||
if (peerChainTipData != null && commonBlockData != null) {
|
||||
PeerChainTipData commonBlockChainTipData = commonBlockData.getChainTipData();
|
||||
if (peerChainTipData.getLastBlockSignature() != null && commonBlockChainTipData != null
|
||||
&& commonBlockChainTipData.getLastBlockSignature() != null) {
|
||||
if (Arrays.equals(peerChainTipData.getLastBlockSignature(),
|
||||
commonBlockChainTipData.getLastBlockSignature())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
CommonBlockData commonBlockData = this.getCommonBlockData();
|
||||
if (commonBlockData == null)
|
||||
return false;
|
||||
|
||||
BlockSummaryData commonBlockChainTipData = commonBlockData.getChainTipData();
|
||||
if (commonBlockChainTipData == null || commonBlockChainTipData.getSignature() == null)
|
||||
return false;
|
||||
|
||||
if (!Arrays.equals(peerChainTipData.getSignature(), commonBlockChainTipData.getSignature()))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -41,6 +41,8 @@ public class AccountMessage extends Message {
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment()));
|
||||
|
||||
bytes.write(Ints.toByteArray(accountData.getBlocksMintedPenalty()));
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
@@ -80,7 +82,9 @@ public class AccountMessage extends Message {
|
||||
|
||||
int blocksMintedAdjustment = byteBuffer.getInt();
|
||||
|
||||
AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
|
||||
int blocksMintedPenalty = byteBuffer.getInt();
|
||||
|
||||
AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty);
|
||||
return new AccountMessage(id, accountData);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,109 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BlockSummariesV2Message extends Message {
|
||||
|
||||
public static final long MINIMUM_PEER_VERSION = 0x0300060001L;
|
||||
|
||||
private static final int BLOCK_SUMMARY_V2_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH /* block signature */
|
||||
+ Transformer.PUBLIC_KEY_LENGTH /* minter public key */
|
||||
+ Transformer.INT_LENGTH /* online accounts count */
|
||||
+ Transformer.LONG_LENGTH /* block timestamp */
|
||||
+ Transformer.INT_LENGTH /* transactions count */
|
||||
+ BlockTransformer.BLOCK_SIGNATURE_LENGTH; /* block reference */
|
||||
|
||||
private List<BlockSummaryData> blockSummaries;
|
||||
|
||||
public BlockSummariesV2Message(List<BlockSummaryData> blockSummaries) {
|
||||
super(MessageType.BLOCK_SUMMARIES_V2);
|
||||
|
||||
// Shortcut for when there are no summaries
|
||||
if (blockSummaries.isEmpty()) {
|
||||
this.dataBytes = Message.EMPTY_DATA_BYTES;
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
// First summary's height
|
||||
bytes.write(Ints.toByteArray(blockSummaries.get(0).getHeight()));
|
||||
|
||||
for (BlockSummaryData blockSummary : blockSummaries) {
|
||||
bytes.write(blockSummary.getSignature());
|
||||
bytes.write(blockSummary.getMinterPublicKey());
|
||||
bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
|
||||
bytes.write(Longs.toByteArray(blockSummary.getTimestamp()));
|
||||
bytes.write(Ints.toByteArray(blockSummary.getTransactionCount()));
|
||||
bytes.write(blockSummary.getReference());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private BlockSummariesV2Message(int id, List<BlockSummaryData> blockSummaries) {
|
||||
super(id, MessageType.BLOCK_SUMMARIES_V2);
|
||||
|
||||
this.blockSummaries = blockSummaries;
|
||||
}
|
||||
|
||||
public List<BlockSummaryData> getBlockSummaries() {
|
||||
return this.blockSummaries;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
|
||||
// If there are no bytes remaining then we can treat this as an empty array of summaries
|
||||
if (bytes.remaining() == 0)
|
||||
return new BlockSummariesV2Message(id, blockSummaries);
|
||||
|
||||
int height = bytes.getInt();
|
||||
|
||||
// Expecting bytes remaining to be exact multiples of BLOCK_SUMMARY_V2_LENGTH
|
||||
if (bytes.remaining() % BLOCK_SUMMARY_V2_LENGTH != 0)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
while (bytes.hasRemaining()) {
|
||||
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
byte[] minterPublicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
bytes.get(minterPublicKey);
|
||||
|
||||
int onlineAccountsCount = bytes.getInt();
|
||||
|
||||
long timestamp = bytes.getLong();
|
||||
|
||||
int transactionsCount = bytes.getInt();
|
||||
|
||||
byte[] reference = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
|
||||
bytes.get(reference);
|
||||
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey,
|
||||
onlineAccountsCount, timestamp, transactionsCount, reference);
|
||||
blockSummaries.add(blockSummary);
|
||||
|
||||
height++;
|
||||
}
|
||||
|
||||
return new BlockSummariesV2Message(id, blockSummaries);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GenericUnknownMessage extends Message {
|
||||
|
||||
public static final long MINIMUM_PEER_VERSION = 0x0300060001L;
|
||||
|
||||
public GenericUnknownMessage() {
|
||||
super(MessageType.GENERIC_UNKNOWN);
|
||||
|
||||
this.dataBytes = EMPTY_DATA_BYTES;
|
||||
}
|
||||
|
||||
private GenericUnknownMessage(int id) {
|
||||
super(id, MessageType.GENERIC_UNKNOWN);
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
return new GenericUnknownMessage(id);
|
||||
}
|
||||
|
||||
}
|
@@ -21,6 +21,7 @@ public enum MessageType {
|
||||
HEIGHT_V2(10, HeightV2Message::fromByteBuffer),
|
||||
PING(11, PingMessage::fromByteBuffer),
|
||||
PONG(12, PongMessage::fromByteBuffer),
|
||||
GENERIC_UNKNOWN(13, GenericUnknownMessage::fromByteBuffer),
|
||||
|
||||
// Requesting data
|
||||
PEERS_V2(20, PeersV2Message::fromByteBuffer),
|
||||
@@ -41,12 +42,13 @@ public enum MessageType {
|
||||
|
||||
BLOCK_SUMMARIES(70, BlockSummariesMessage::fromByteBuffer),
|
||||
GET_BLOCK_SUMMARIES(71, GetBlockSummariesMessage::fromByteBuffer),
|
||||
BLOCK_SUMMARIES_V2(72, BlockSummariesV2Message::fromByteBuffer),
|
||||
|
||||
ONLINE_ACCOUNTS(80, OnlineAccountsMessage::fromByteBuffer),
|
||||
GET_ONLINE_ACCOUNTS(81, GetOnlineAccountsMessage::fromByteBuffer),
|
||||
ONLINE_ACCOUNTS_V2(82, OnlineAccountsV2Message::fromByteBuffer),
|
||||
GET_ONLINE_ACCOUNTS_V2(83, GetOnlineAccountsV2Message::fromByteBuffer),
|
||||
// ONLINE_ACCOUNTS_V3(84, OnlineAccountsV3Message::fromByteBuffer),
|
||||
ONLINE_ACCOUNTS_V3(84, OnlineAccountsV3Message::fromByteBuffer),
|
||||
GET_ONLINE_ACCOUNTS_V3(85, GetOnlineAccountsV3Message::fromByteBuffer),
|
||||
|
||||
ARBITRARY_DATA(90, ArbitraryDataMessage::fromByteBuffer),
|
||||
|
@@ -0,0 +1,121 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* For sending online accounts info to remote peer.
|
||||
*
|
||||
* Same format as V2, but with added support for a mempow nonce.
|
||||
*/
|
||||
public class OnlineAccountsV3Message extends Message {
|
||||
|
||||
public static final long MIN_PEER_VERSION = 0x300060000L; // 3.6.0
|
||||
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
|
||||
public OnlineAccountsV3Message(List<OnlineAccountData> onlineAccounts) {
|
||||
super(MessageType.ONLINE_ACCOUNTS_V3);
|
||||
|
||||
// Shortcut in case we have no online accounts
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
this.dataBytes = Ints.toByteArray(0);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
// How many of each timestamp
|
||||
Map<Long, Integer> countByTimestamp = new HashMap<>();
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
Long timestamp = onlineAccountData.getTimestamp();
|
||||
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
|
||||
+ onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
try {
|
||||
for (long timestamp : countByTimestamp.keySet()) {
|
||||
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
if (onlineAccountData.getTimestamp() == timestamp) {
|
||||
bytes.write(onlineAccountData.getSignature());
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
|
||||
// Nonce is optional; use -1 as placeholder if missing
|
||||
int nonce = onlineAccountData.getNonce() != null ? onlineAccountData.getNonce() : -1;
|
||||
bytes.write(Ints.toByteArray(nonce));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private OnlineAccountsV3Message(int id, List<OnlineAccountData> onlineAccounts) {
|
||||
super(id, MessageType.ONLINE_ACCOUNTS_V3);
|
||||
|
||||
this.onlineAccounts = onlineAccounts;
|
||||
}
|
||||
|
||||
public List<OnlineAccountData> getOnlineAccounts() {
|
||||
return this.onlineAccounts;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
int accountCount = bytes.getInt();
|
||||
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
|
||||
|
||||
while (accountCount > 0) {
|
||||
long timestamp = bytes.getLong();
|
||||
|
||||
for (int i = 0; i < accountCount; ++i) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
bytes.get(publicKey);
|
||||
|
||||
// Nonce is optional - will be -1 if missing
|
||||
Integer nonce = bytes.getInt();
|
||||
if (nonce < 0) {
|
||||
nonce = null;
|
||||
}
|
||||
|
||||
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey, nonce));
|
||||
}
|
||||
|
||||
if (bytes.hasRemaining()) {
|
||||
accountCount = bytes.getInt();
|
||||
} else {
|
||||
// we've finished
|
||||
accountCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return new OnlineAccountsV3Message(id, onlineAccounts);
|
||||
}
|
||||
|
||||
}
|
@@ -1,13 +1,9 @@
|
||||
package org.qortal.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.EligibleQoraHolderData;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.QortFromQoraData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.account.*;
|
||||
|
||||
public interface AccountRepository {
|
||||
|
||||
@@ -19,6 +15,9 @@ public interface AccountRepository {
|
||||
/** Returns accounts with <b>any</b> bit set in given mask. */
|
||||
public List<AccountData> getFlaggedAccounts(int mask) throws DataException;
|
||||
|
||||
/** Returns accounts with a blockedMintedPenalty */
|
||||
public List<AccountData> getPenaltyAccounts() throws DataException;
|
||||
|
||||
/** Returns account's last reference or null if not set or account not found. */
|
||||
public byte[] getLastReference(String address) throws DataException;
|
||||
|
||||
@@ -100,6 +99,18 @@ public interface AccountRepository {
|
||||
*/
|
||||
public void modifyMintedBlockCounts(List<String> addresses, int delta) throws DataException;
|
||||
|
||||
/** Returns account's block minted penalty count or null if account not found. */
|
||||
public Integer getBlocksMintedPenaltyCount(String address) throws DataException;
|
||||
|
||||
/**
|
||||
* Sets blocks minted penalties for given list of accounts.
|
||||
* This replaces the existing values rather than modifying them by a delta.
|
||||
*
|
||||
* @param accountPenalties
|
||||
* @throws DataException
|
||||
*/
|
||||
public void updateBlocksMintedPenalties(Set<AccountPenaltyData> accountPenalties) throws DataException;
|
||||
|
||||
/** Delete account from repository. */
|
||||
public void delete(String address) throws DataException;
|
||||
|
||||
|
@@ -24,7 +24,7 @@ public interface ArbitraryRepository {
|
||||
public ArbitraryTransactionData getLatestTransaction(String name, Service service, Method method, String identifier) throws DataException;
|
||||
|
||||
|
||||
public List<ArbitraryResourceInfo> getArbitraryResources(Service service, String identifier, String name, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
public List<ArbitraryResourceInfo> getArbitraryResources(Service service, String identifier, List<String> names, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public List<ArbitraryResourceInfo> searchArbitraryResources(Service service, String query, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
|
@@ -14,8 +14,8 @@ public interface ChatRepository {
|
||||
* Expects EITHER non-null txGroupID OR non-null sender and recipient addresses.
|
||||
*/
|
||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after,
|
||||
Integer txGroupId, List<String> involving,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
Integer txGroupId, byte[] reference, byte[] chatReferenceBytes, Boolean hasChatReference,
|
||||
List<String> involving, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException;
|
||||
|
||||
|
@@ -131,7 +131,14 @@ public interface GroupRepository {
|
||||
|
||||
public GroupBanData getBan(int groupId, String member) throws DataException;
|
||||
|
||||
public boolean banExists(int groupId, String offender) throws DataException;
|
||||
/**
|
||||
* IMPORTANT: when using banExists() as part of validation, the timestamp must be that of the transaction that
|
||||
* is calling banExists() as part of its validation. It must NOT be the current time, unless this is being
|
||||
* called outside of validation, as part of an on demand check for a ban existing (such as via an API call).
|
||||
* This is because we need to evaluate a ban's status based on the time of the subsequent transaction, as
|
||||
* validation will not occur at a fixed time for every node. For some, it could be months into the future.
|
||||
*/
|
||||
public boolean banExists(int groupId, String offender, long timestamp) throws DataException;
|
||||
|
||||
public List<GroupBanData> getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
|
@@ -179,6 +179,15 @@ public interface TransactionRepository {
|
||||
public List<TransferAssetTransactionData> getAssetTransfers(long assetId, String address, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of reward share transaction creators, excluding self shares.
|
||||
* This uses confirmed transactions only.
|
||||
*
|
||||
* @return
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<String> getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException;
|
||||
|
||||
/**
|
||||
* Returns list of transactions pending approval, with optional txGgroupId filtering.
|
||||
* <p>
|
||||
|
@@ -6,15 +6,11 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.EligibleQoraHolderData;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.QortFromQoraData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.account.*;
|
||||
import org.qortal.repository.AccountRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
@@ -30,7 +26,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public AccountData getAccount(String address) throws DataException {
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment FROM Accounts WHERE account = ?";
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty FROM Accounts WHERE account = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
||||
if (resultSet == null)
|
||||
@@ -43,8 +39,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
int level = resultSet.getInt(5);
|
||||
int blocksMinted = resultSet.getInt(6);
|
||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||
int blocksMintedPenalty = resultSet.getInt(8);
|
||||
|
||||
return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
|
||||
return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch account info from repository", e);
|
||||
}
|
||||
@@ -52,7 +49,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
|
||||
@Override
|
||||
public List<AccountData> getFlaggedAccounts(int mask) throws DataException {
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, account FROM Accounts WHERE BITAND(flags, ?) != 0";
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty, account FROM Accounts WHERE BITAND(flags, ?) != 0";
|
||||
|
||||
List<AccountData> accounts = new ArrayList<>();
|
||||
|
||||
@@ -68,9 +65,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
int level = resultSet.getInt(5);
|
||||
int blocksMinted = resultSet.getInt(6);
|
||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||
String address = resultSet.getString(8);
|
||||
int blocksMintedPenalty = resultSet.getInt(8);
|
||||
String address = resultSet.getString(9);
|
||||
|
||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment));
|
||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty));
|
||||
} while (resultSet.next());
|
||||
|
||||
return accounts;
|
||||
@@ -79,6 +77,36 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AccountData> getPenaltyAccounts() throws DataException {
|
||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty, account FROM Accounts WHERE blocks_minted_penalty != 0";
|
||||
|
||||
List<AccountData> accounts = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return accounts;
|
||||
|
||||
do {
|
||||
byte[] reference = resultSet.getBytes(1);
|
||||
byte[] publicKey = resultSet.getBytes(2);
|
||||
int defaultGroupId = resultSet.getInt(3);
|
||||
int flags = resultSet.getInt(4);
|
||||
int level = resultSet.getInt(5);
|
||||
int blocksMinted = resultSet.getInt(6);
|
||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||
int blocksMintedPenalty = resultSet.getInt(8);
|
||||
String address = resultSet.getString(9);
|
||||
|
||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty));
|
||||
} while (resultSet.next());
|
||||
|
||||
return accounts;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch penalty accounts from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getLastReference(String address) throws DataException {
|
||||
String sql = "SELECT reference FROM Accounts WHERE account = ?";
|
||||
@@ -298,6 +326,39 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getBlocksMintedPenaltyCount(String address) throws DataException {
|
||||
String sql = "SELECT blocks_minted_penalty FROM Accounts WHERE account = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
return resultSet.getInt(1);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch account's block minted penalty count from repository", e);
|
||||
}
|
||||
}
|
||||
public void updateBlocksMintedPenalties(Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||
// Nothing to do?
|
||||
if (accountPenalties == null || accountPenalties.isEmpty())
|
||||
return;
|
||||
|
||||
// Map balance changes into SQL bind params, filtering out no-op changes
|
||||
List<Object[]> updateBlocksMintedPenaltyParams = accountPenalties.stream()
|
||||
.map(accountPenalty -> new Object[] { accountPenalty.getAddress(), accountPenalty.getBlocksMintedPenalty(), accountPenalty.getBlocksMintedPenalty() })
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Perform actual balance changes
|
||||
String sql = "INSERT INTO Accounts (account, blocks_minted_penalty) VALUES (?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE blocks_minted_penalty = blocks_minted_penalty + ?";
|
||||
try {
|
||||
this.repository.executeCheckedBatchUpdate(sql, updateBlocksMintedPenaltyParams);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to set blocks minted penalties in repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String address) throws DataException {
|
||||
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
|
||||
|
@@ -301,7 +301,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArbitraryResourceInfo> getArbitraryResources(Service service, String identifier, String name,
|
||||
public List<ArbitraryResourceInfo> getArbitraryResources(Service service, String identifier, List<String> names,
|
||||
boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
@@ -325,9 +325,16 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
bindParams.add(identifier);
|
||||
}
|
||||
|
||||
if (name != null) {
|
||||
sql.append(" AND name = ?");
|
||||
bindParams.add(name);
|
||||
if (names != null && !names.isEmpty()) {
|
||||
sql.append(" AND name IN (?");
|
||||
bindParams.add(names.get(0));
|
||||
|
||||
for (int i = 1; i < names.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(names.get(i));
|
||||
}
|
||||
|
||||
sql.append(")");
|
||||
}
|
||||
|
||||
sql.append(" GROUP BY name, service, identifier ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||
|
@@ -143,13 +143,17 @@ public class HSQLDBBlockArchiveRepository implements BlockArchiveRepository {
|
||||
byte[] blockMinterPublicKey = resultSet.getBytes(3);
|
||||
|
||||
// Fetch additional info from the archive itself
|
||||
int onlineAccountsCount = 0;
|
||||
Integer onlineAccountsCount = null;
|
||||
Long timestamp = null;
|
||||
Integer transactionCount = null;
|
||||
byte[] reference = null;
|
||||
|
||||
BlockData blockData = this.fromSignature(signature);
|
||||
if (blockData != null) {
|
||||
onlineAccountsCount = blockData.getOnlineAccountsCount();
|
||||
}
|
||||
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount);
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount, timestamp, transactionCount, reference);
|
||||
blockSummaries.add(blockSummary);
|
||||
} while (resultSet.next());
|
||||
|
||||
|
@@ -297,7 +297,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
@Override
|
||||
public List<BlockSummaryData> getBlockSummariesBySigner(byte[] signerPublicKey, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
sql.append("SELECT signature, height, Blocks.minter, online_accounts_count FROM ");
|
||||
sql.append("SELECT signature, height, Blocks.minter, online_accounts_count, minted_when, transaction_count, Blocks.reference FROM ");
|
||||
|
||||
// List of minter account's public key and reward-share public keys with minter's public key
|
||||
sql.append("(SELECT * FROM (VALUES (CAST(? AS QortalPublicKey))) UNION (SELECT reward_share_public_key FROM RewardShares WHERE minter_public_key = ?)) AS PublicKeys (public_key) ");
|
||||
@@ -322,8 +322,12 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
int height = resultSet.getInt(2);
|
||||
byte[] blockMinterPublicKey = resultSet.getBytes(3);
|
||||
int onlineAccountsCount = resultSet.getInt(4);
|
||||
long timestamp = resultSet.getLong(5);
|
||||
int transactionCount = resultSet.getInt(6);
|
||||
byte[] reference = resultSet.getBytes(7);
|
||||
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount);
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount,
|
||||
timestamp, transactionCount, reference);
|
||||
blockSummaries.add(blockSummary);
|
||||
} while (resultSet.next());
|
||||
|
||||
@@ -355,7 +359,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
|
||||
@Override
|
||||
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException {
|
||||
String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count "
|
||||
String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count, reference "
|
||||
+ "FROM Blocks WHERE height BETWEEN ? AND ?";
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
@@ -371,9 +375,10 @@ public class HSQLDBBlockRepository implements BlockRepository {
|
||||
int onlineAccountsCount = resultSet.getInt(4);
|
||||
long timestamp = resultSet.getLong(5);
|
||||
int transactionCount = resultSet.getInt(6);
|
||||
byte[] reference = resultSet.getBytes(7);
|
||||
|
||||
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount,
|
||||
timestamp, transactionCount);
|
||||
timestamp, transactionCount, reference);
|
||||
blockSummaries.add(blockSummary);
|
||||
} while (resultSet.next());
|
||||
|
||||
|
@@ -23,9 +23,9 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId,
|
||||
List<String> involving, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException {
|
||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes,
|
||||
byte[] chatReferenceBytes, Boolean hasChatReference, List<String> involving,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
// Check args meet expectations
|
||||
if ((txGroupId != null && involving != null && !involving.isEmpty())
|
||||
|| (txGroupId == null && (involving == null || involving.size() != 2)))
|
||||
@@ -35,7 +35,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
|
||||
sql.append("SELECT created_when, tx_group_id, Transactions.reference, creator, "
|
||||
+ "sender, SenderNames.name, recipient, RecipientNames.name, "
|
||||
+ "data, is_text, is_encrypted, signature "
|
||||
+ "chat_reference, data, is_text, is_encrypted, signature "
|
||||
+ "FROM ChatTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
||||
@@ -57,6 +57,23 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
bindParams.add(after);
|
||||
}
|
||||
|
||||
if (referenceBytes != null) {
|
||||
whereClauses.add("reference = ?");
|
||||
bindParams.add(referenceBytes);
|
||||
}
|
||||
|
||||
if (chatReferenceBytes != null) {
|
||||
whereClauses.add("chat_reference = ?");
|
||||
bindParams.add(chatReferenceBytes);
|
||||
}
|
||||
|
||||
if (hasChatReference != null && hasChatReference == true) {
|
||||
whereClauses.add("chat_reference IS NOT NULL");
|
||||
}
|
||||
else if (hasChatReference != null && hasChatReference == false) {
|
||||
whereClauses.add("chat_reference IS NULL");
|
||||
}
|
||||
|
||||
if (txGroupId != null) {
|
||||
whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally
|
||||
whereClauses.add("recipient IS NULL");
|
||||
@@ -98,13 +115,14 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
String senderName = resultSet.getString(6);
|
||||
String recipient = resultSet.getString(7);
|
||||
String recipientName = resultSet.getString(8);
|
||||
byte[] data = resultSet.getBytes(9);
|
||||
boolean isText = resultSet.getBoolean(10);
|
||||
boolean isEncrypted = resultSet.getBoolean(11);
|
||||
byte[] signature = resultSet.getBytes(12);
|
||||
byte[] chatReference = resultSet.getBytes(9);
|
||||
byte[] data = resultSet.getBytes(10);
|
||||
boolean isText = resultSet.getBoolean(11);
|
||||
boolean isEncrypted = resultSet.getBoolean(12);
|
||||
byte[] signature = resultSet.getBytes(13);
|
||||
|
||||
ChatMessage chatMessage = new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
||||
senderName, recipient, recipientName, data, isText, isEncrypted, signature);
|
||||
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
|
||||
|
||||
chatMessages.add(chatMessage);
|
||||
} while (resultSet.next());
|
||||
@@ -136,13 +154,14 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
byte[] senderPublicKey = chatTransactionData.getSenderPublicKey();
|
||||
String sender = chatTransactionData.getSender();
|
||||
String recipient = chatTransactionData.getRecipient();
|
||||
byte[] chatReference = chatTransactionData.getChatReference();
|
||||
byte[] data = chatTransactionData.getData();
|
||||
boolean isText = chatTransactionData.getIsText();
|
||||
boolean isEncrypted = chatTransactionData.getIsEncrypted();
|
||||
byte[] signature = chatTransactionData.getSignature();
|
||||
|
||||
return new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
||||
senderName, recipient, recipientName, data, isText, isEncrypted, signature);
|
||||
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch convert chat transaction from repository", e);
|
||||
}
|
||||
|
@@ -975,6 +975,19 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN receiving_account_info SET DATA TYPE VARBINARY(128)");
|
||||
break;
|
||||
|
||||
case 44:
|
||||
// Add blocks minted penalty
|
||||
stmt.execute("ALTER TABLE Accounts ADD blocks_minted_penalty INTEGER NOT NULL DEFAULT 0");
|
||||
break;
|
||||
|
||||
case 45:
|
||||
// Add a chat reference, to allow one message to reference another, and for this to be easily
|
||||
// searchable. Null values are allowed as most transactions won't have a reference.
|
||||
stmt.execute("ALTER TABLE ChatTransactions ADD chat_reference Signature");
|
||||
// For finding chat messages by reference
|
||||
stmt.execute("CREATE INDEX ChatTransactionsChatReferenceIndex ON ChatTransactions (chat_reference)");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@@ -777,9 +777,9 @@ public class HSQLDBGroupRepository implements GroupRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean banExists(int groupId, String offender) throws DataException {
|
||||
public boolean banExists(int groupId, String offender, long timestamp) throws DataException {
|
||||
try {
|
||||
return this.repository.exists("GroupBans", "group_id = ? AND offender = ?", groupId, offender);
|
||||
return this.repository.exists("GroupBans", "group_id = ? AND offender = ? AND (expires_when IS NULL OR expires_when > ?)", groupId, offender, timestamp);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to check for group ban in repository", e);
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository
|
||||
}
|
||||
|
||||
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
|
||||
String sql = "SELECT sender, nonce, recipient, is_text, is_encrypted, data FROM ChatTransactions WHERE signature = ?";
|
||||
String sql = "SELECT sender, nonce, recipient, is_text, is_encrypted, data, chat_reference FROM ChatTransactions WHERE signature = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
|
||||
if (resultSet == null)
|
||||
@@ -29,8 +29,9 @@ public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository
|
||||
boolean isText = resultSet.getBoolean(4);
|
||||
boolean isEncrypted = resultSet.getBoolean(5);
|
||||
byte[] data = resultSet.getBytes(6);
|
||||
byte[] chatReference = resultSet.getBytes(7);
|
||||
|
||||
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, data, isText, isEncrypted);
|
||||
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, chatReference, data, isText, isEncrypted);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch chat transaction from repository", e);
|
||||
}
|
||||
@@ -45,7 +46,7 @@ public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository
|
||||
saveHelper.bind("signature", chatTransactionData.getSignature()).bind("nonce", chatTransactionData.getNonce())
|
||||
.bind("sender", chatTransactionData.getSender()).bind("recipient", chatTransactionData.getRecipient())
|
||||
.bind("is_text", chatTransactionData.getIsText()).bind("is_encrypted", chatTransactionData.getIsEncrypted())
|
||||
.bind("data", chatTransactionData.getData());
|
||||
.bind("data", chatTransactionData.getData()).bind("chat_reference", chatTransactionData.getChatReference());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
|
@@ -7,11 +7,7 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -969,6 +965,33 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException {
|
||||
List<String> rewardShareCreators = new ArrayList<>();
|
||||
|
||||
String sql = "SELECT account "
|
||||
+ "FROM RewardShareTransactions "
|
||||
+ "JOIN Accounts ON Accounts.public_key = RewardShareTransactions.minter_public_key "
|
||||
+ "JOIN Transactions ON Transactions.signature = RewardShareTransactions.signature "
|
||||
+ "WHERE block_height IS NOT NULL AND RewardShareTransactions.recipient != Accounts.account "
|
||||
+ "GROUP BY account "
|
||||
+ "ORDER BY account";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||
if (resultSet == null)
|
||||
return rewardShareCreators;
|
||||
|
||||
do {
|
||||
String address = resultSet.getString(1);
|
||||
|
||||
rewardShareCreators.add(address);
|
||||
} while (resultSet.next());
|
||||
|
||||
return rewardShareCreators;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch reward share creators from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TransactionData> getApprovalPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
|
@@ -110,7 +110,13 @@ public class Settings {
|
||||
/** Maximum number of unconfirmed transactions allowed per account */
|
||||
private int maxUnconfirmedPerAccount = 25;
|
||||
/** Max milliseconds into future for accepting new, unconfirmed transactions */
|
||||
private int maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds
|
||||
private int maxTransactionTimestampFuture = 30 * 60 * 1000; // milliseconds
|
||||
|
||||
/** Maximum number of CHAT transactions allowed per account in recent timeframe */
|
||||
private int maxRecentChatMessagesPerAccount = 250;
|
||||
/** Maximum age of a CHAT transaction to be considered 'recent' */
|
||||
private long recentChatMessagesMaxAge = 60 * 60 * 1000L; // milliseconds
|
||||
|
||||
/** Whether we check, fetch and install auto-updates */
|
||||
private boolean autoUpdateEnabled = true;
|
||||
/** How long between repository backups (ms), or 0 if disabled. */
|
||||
@@ -153,7 +159,7 @@ public class Settings {
|
||||
* This prevents the node from being able to serve older blocks */
|
||||
private boolean topOnly = false;
|
||||
/** The amount of recent blocks we should keep when pruning */
|
||||
private int pruneBlockLimit = 1450;
|
||||
private int pruneBlockLimit = 6000;
|
||||
|
||||
/** How often to attempt AT state pruning (ms). */
|
||||
private long atStatesPruneInterval = 3219L; // milliseconds
|
||||
@@ -184,6 +190,8 @@ public class Settings {
|
||||
|
||||
// Peer-to-peer related
|
||||
private boolean isTestNet = false;
|
||||
/** Single node testnet mode */
|
||||
private boolean singleNodeTestnet = false;
|
||||
/** Port number for inbound peer-to-peer connections. */
|
||||
private Integer listenPort;
|
||||
/** Whether to attempt to open the listen port via UPnP */
|
||||
@@ -203,8 +211,11 @@ public class Settings {
|
||||
/** Maximum number of retry attempts if a peer fails to respond with the requested data */
|
||||
private int maxRetries = 2;
|
||||
|
||||
/** The number of seconds of no activity before recovery mode begins */
|
||||
public long recoveryModeTimeout = 10 * 60 * 1000L;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "3.3.7";
|
||||
private String minPeerVersion = "3.8.2";
|
||||
/** Whether to allow connections with peers below minPeerVersion
|
||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||
@@ -262,7 +273,7 @@ public class Settings {
|
||||
private String[] bootstrapHosts = new String[] {
|
||||
"http://bootstrap.qortal.org",
|
||||
"http://bootstrap2.qortal.org",
|
||||
"http://62.171.190.193"
|
||||
"http://bootstrap.qortal.online"
|
||||
};
|
||||
|
||||
// Auto-update sources
|
||||
@@ -291,6 +302,7 @@ public class Settings {
|
||||
private Long testNtpOffset = null;
|
||||
|
||||
|
||||
|
||||
/* Foreign chains */
|
||||
|
||||
/** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */
|
||||
@@ -485,7 +497,7 @@ public class Settings {
|
||||
|
||||
private void validate() {
|
||||
// Validation goes here
|
||||
if (this.minBlockchainPeers < 1)
|
||||
if (this.minBlockchainPeers < 1 && !singleNodeTestnet)
|
||||
throwValidationError("minBlockchainPeers must be at least 1");
|
||||
|
||||
if (this.apiKey != null && this.apiKey.trim().length() < 8)
|
||||
@@ -634,6 +646,14 @@ public class Settings {
|
||||
return this.maxTransactionTimestampFuture;
|
||||
}
|
||||
|
||||
public int getMaxRecentChatMessagesPerAccount() {
|
||||
return this.maxRecentChatMessagesPerAccount;
|
||||
}
|
||||
|
||||
public long getRecentChatMessagesMaxAge() {
|
||||
return recentChatMessagesMaxAge;
|
||||
}
|
||||
|
||||
public int getBlockCacheSize() {
|
||||
return this.blockCacheSize;
|
||||
}
|
||||
@@ -642,6 +662,10 @@ public class Settings {
|
||||
return this.isTestNet;
|
||||
}
|
||||
|
||||
public boolean isSingleNodeTestnet() {
|
||||
return this.singleNodeTestnet;
|
||||
}
|
||||
|
||||
public int getListenPort() {
|
||||
if (this.listenPort != null)
|
||||
return this.listenPort;
|
||||
@@ -662,6 +686,9 @@ public class Settings {
|
||||
}
|
||||
|
||||
public int getMinBlockchainPeers() {
|
||||
if (singleNodeTestnet)
|
||||
return 0;
|
||||
|
||||
return this.minBlockchainPeers;
|
||||
}
|
||||
|
||||
@@ -687,6 +714,10 @@ public class Settings {
|
||||
|
||||
public int getMaxRetries() { return this.maxRetries; }
|
||||
|
||||
public long getRecoveryModeTimeout() {
|
||||
return recoveryModeTimeout;
|
||||
}
|
||||
|
||||
public String getMinPeerVersion() { return this.minPeerVersion; }
|
||||
|
||||
public boolean getAllowConnectionsWithOlderPeerVersions() { return this.allowConnectionsWithOlderPeerVersions; }
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.transaction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
@@ -64,15 +65,24 @@ public class AddGroupAdminTransaction extends Transaction {
|
||||
|
||||
Account owner = getOwner();
|
||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||
|
||||
// Check transaction's public key matches group's current owner
|
||||
if (!owner.getAddress().equals(groupOwner))
|
||||
// Require approval if transaction relates to a group owned by the null account
|
||||
if (groupOwnedByNullAccount && !this.needsGroupApproval())
|
||||
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||
|
||||
// Check transaction's public key matches group's current owner (except for groups owned by the null account)
|
||||
if (!groupOwnedByNullAccount && !owner.getAddress().equals(groupOwner))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
|
||||
// Check address is a group member
|
||||
if (!this.repository.getGroupRepository().memberExists(groupId, memberAddress))
|
||||
return ValidationResult.NOT_GROUP_MEMBER;
|
||||
|
||||
// Check transaction creator is a group member
|
||||
if (!this.repository.getGroupRepository().memberExists(groupId, this.getCreator().getAddress()))
|
||||
return ValidationResult.NOT_GROUP_MEMBER;
|
||||
|
||||
// Check group member is not already an admin
|
||||
if (this.repository.getGroupRepository().adminExists(groupId, memberAddress))
|
||||
return ValidationResult.ALREADY_GROUP_ADMIN;
|
||||
|
@@ -24,6 +24,7 @@ import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class ArbitraryTransaction extends Transaction {
|
||||
|
||||
@@ -34,9 +35,13 @@ public class ArbitraryTransaction extends Transaction {
|
||||
public static final int MAX_DATA_SIZE = 4000;
|
||||
public static final int MAX_METADATA_LENGTH = 32;
|
||||
public static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
public static final int MAX_IDENTIFIER_LENGTH = 64;
|
||||
|
||||
/** If time difference between transaction and now is greater than this then we don't verify proof-of-work. */
|
||||
public static final long HISTORIC_THRESHOLD = 2 * 7 * 24 * 60 * 60 * 1000L;
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
|
||||
|
||||
// Constructors
|
||||
|
||||
public ArbitraryTransaction(Repository repository, TransactionData transactionData) {
|
||||
@@ -202,9 +207,11 @@ public class ArbitraryTransaction extends Transaction {
|
||||
// Clear nonce from transactionBytes
|
||||
ArbitraryTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// Check nonce
|
||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
// We only need to check nonce for recent transactions due to PoW verification overhead
|
||||
if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) {
|
||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@@ -73,7 +73,7 @@ public class CancelGroupBanTransaction extends Transaction {
|
||||
Account member = getMember();
|
||||
|
||||
// Check ban actually exists
|
||||
if (!this.repository.getGroupRepository().banExists(groupId, member.getAddress()))
|
||||
if (!this.repository.getGroupRepository().banExists(groupId, member.getAddress(), this.groupUnbanTransactionData.getTimestamp()))
|
||||
return ValidationResult.BAN_UNKNOWN;
|
||||
|
||||
// Check admin has enough funds
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package org.qortal.transaction;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
@@ -16,9 +18,11 @@ import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ChatTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class ChatTransaction extends Transaction {
|
||||
|
||||
@@ -26,10 +30,11 @@ public class ChatTransaction extends Transaction {
|
||||
private ChatTransactionData chatTransactionData;
|
||||
|
||||
// Other useful constants
|
||||
public static final int MAX_DATA_SIZE = 256;
|
||||
public static final int MAX_DATA_SIZE = 4000;
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
public static final int POW_DIFFICULTY_WITH_QORT = 8; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_NO_QORT = 12; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_ABOVE_QORT_THRESHOLD = 8; // leading zero bits
|
||||
public static final int POW_DIFFICULTY_BELOW_QORT_THRESHOLD = 18; // leading zero bits
|
||||
public static final long POW_QORT_THRESHOLD = 400000000L;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -78,7 +83,7 @@ public class ChatTransaction extends Transaction {
|
||||
// Clear nonce from transactionBytes
|
||||
ChatTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
int difficulty = this.getSender().getConfirmedBalance(Asset.QORT) > 0 ? POW_DIFFICULTY_WITH_QORT : POW_DIFFICULTY_NO_QORT;
|
||||
int difficulty = this.getSender().getConfirmedBalance(Asset.QORT) >= POW_QORT_THRESHOLD ? POW_DIFFICULTY_ABOVE_QORT_THRESHOLD : POW_DIFFICULTY_BELOW_QORT_THRESHOLD;
|
||||
|
||||
// Calculate nonce
|
||||
this.chatTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, difficulty));
|
||||
@@ -145,6 +150,11 @@ public class ChatTransaction extends Transaction {
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
|
||||
// Disregard messages with timestamp too far in the future (we have stricter limits for CHAT transactions)
|
||||
if (this.chatTransactionData.getTimestamp() > NTP.getTime() + (5 * 60 * 1000L)) {
|
||||
return ValidationResult.TIMESTAMP_TOO_NEW;
|
||||
}
|
||||
|
||||
// Check for blocked author by address
|
||||
ResourceListManager listManager = ResourceListManager.getInstance();
|
||||
if (listManager.listContains("blockedAddresses", this.chatTransactionData.getSender(), true)) {
|
||||
@@ -163,6 +173,14 @@ public class ChatTransaction extends Transaction {
|
||||
}
|
||||
}
|
||||
|
||||
PublicKeyAccount creator = this.getCreator();
|
||||
if (creator == null)
|
||||
return ValidationResult.MISSING_CREATOR;
|
||||
|
||||
// Reject if unconfirmed pile already has X recent CHAT transactions from same creator
|
||||
if (countRecentChatTransactionsByCreator(creator) >= Settings.getInstance().getMaxRecentChatMessagesPerAccount())
|
||||
return ValidationResult.TOO_MANY_UNCONFIRMED;
|
||||
|
||||
// If we exist in the repository then we've been imported as unconfirmed,
|
||||
// but we don't want to make it into a block, so return fake non-OK result.
|
||||
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))
|
||||
@@ -204,7 +222,7 @@ public class ChatTransaction extends Transaction {
|
||||
|
||||
int difficulty;
|
||||
try {
|
||||
difficulty = this.getSender().getConfirmedBalance(Asset.QORT) > 0 ? POW_DIFFICULTY_WITH_QORT : POW_DIFFICULTY_NO_QORT;
|
||||
difficulty = this.getSender().getConfirmedBalance(Asset.QORT) >= POW_QORT_THRESHOLD ? POW_DIFFICULTY_ABOVE_QORT_THRESHOLD : POW_DIFFICULTY_BELOW_QORT_THRESHOLD;
|
||||
} catch (DataException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -213,6 +231,26 @@ public class ChatTransaction extends Transaction {
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
}
|
||||
|
||||
private int countRecentChatTransactionsByCreator(PublicKeyAccount creator) throws DataException {
|
||||
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||
final Long now = NTP.getTime();
|
||||
long recentThreshold = Settings.getInstance().getRecentChatMessagesMaxAge();
|
||||
|
||||
// We only care about chat transactions, and only those that are considered 'recent'
|
||||
Predicate<TransactionData> hasSameCreatorAndIsRecentChat = transactionData -> {
|
||||
if (transactionData.getType() != TransactionType.CHAT)
|
||||
return false;
|
||||
|
||||
if (transactionData.getTimestamp() < now - recentThreshold)
|
||||
return false;
|
||||
|
||||
return Arrays.equals(creator.getPublicKey(), transactionData.getCreatorPublicKey());
|
||||
};
|
||||
|
||||
return (int) unconfirmedTransactions.stream().filter(hasSameCreatorAndIsRecentChat).count();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure there's at least a skeleton account so people
|
||||
* can retrieve sender's public key using address, even if all their messages
|
||||
|
@@ -78,7 +78,7 @@ public class GroupInviteTransaction extends Transaction {
|
||||
return ValidationResult.ALREADY_GROUP_MEMBER;
|
||||
|
||||
// Check invitee is not banned
|
||||
if (this.repository.getGroupRepository().banExists(groupId, invitee.getAddress()))
|
||||
if (this.repository.getGroupRepository().banExists(groupId, invitee.getAddress(), this.groupInviteTransactionData.getTimestamp()))
|
||||
return ValidationResult.BANNED_FROM_GROUP;
|
||||
|
||||
// Check creator has enough funds
|
||||
|
@@ -53,7 +53,7 @@ public class JoinGroupTransaction extends Transaction {
|
||||
return ValidationResult.ALREADY_GROUP_MEMBER;
|
||||
|
||||
// Check member is not banned
|
||||
if (this.repository.getGroupRepository().banExists(groupId, joiner.getAddress()))
|
||||
if (this.repository.getGroupRepository().banExists(groupId, joiner.getAddress(), this.joinGroupTransactionData.getTimestamp()))
|
||||
return ValidationResult.BANNED_FROM_GROUP;
|
||||
|
||||
// Check join request doesn't already exist
|
||||
|
@@ -4,7 +4,9 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -26,7 +28,7 @@ public class PublicizeTransaction extends Transaction {
|
||||
/** If time difference between transaction and now is greater than this then we don't verify proof-of-work. */
|
||||
public static final long HISTORIC_THRESHOLD = 2 * 7 * 24 * 60 * 60 * 1000L;
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
public static final int POW_DIFFICULTY = 15; // leading zero bits
|
||||
public static final int POW_DIFFICULTY = 14; // leading zero bits
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -102,6 +104,12 @@ public class PublicizeTransaction extends Transaction {
|
||||
if (!verifyNonce())
|
||||
return ValidationResult.INCORRECT_NONCE;
|
||||
|
||||
// Validate fee if one has been included
|
||||
PublicKeyAccount creator = this.getCreator();
|
||||
if (this.transactionData.getFee() > 0)
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.transactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.transaction;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
@@ -65,11 +66,21 @@ public class RemoveGroupAdminTransaction extends Transaction {
|
||||
return ValidationResult.GROUP_DOES_NOT_EXIST;
|
||||
|
||||
Account owner = getOwner();
|
||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||
|
||||
// Check transaction's public key matches group's current owner
|
||||
if (!owner.getAddress().equals(groupData.getOwner()))
|
||||
// Require approval if transaction relates to a group owned by the null account
|
||||
if (groupOwnedByNullAccount && !this.needsGroupApproval())
|
||||
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||
|
||||
// Check transaction's public key matches group's current owner (except for groups owned by the null account)
|
||||
if (!groupOwnedByNullAccount && !owner.getAddress().equals(groupOwner))
|
||||
return ValidationResult.INVALID_GROUP_OWNER;
|
||||
|
||||
// Check transaction creator is a group member
|
||||
if (!this.repository.getGroupRepository().memberExists(groupId, this.getCreator().getAddress()))
|
||||
return ValidationResult.NOT_GROUP_MEMBER;
|
||||
|
||||
Account admin = getAdmin();
|
||||
|
||||
// Check member is an admin
|
||||
|
@@ -163,9 +163,12 @@ public class RewardShareTransaction extends Transaction {
|
||||
return ValidationResult.SELF_SHARE_EXISTS;
|
||||
}
|
||||
|
||||
// Fee checking needed if not setting up new self-share
|
||||
if (!(isRecipientAlsoMinter && existingRewardShareData == null))
|
||||
// Check creator has enough funds
|
||||
// Check creator has enough funds
|
||||
if (this.rewardShareTransactionData.getTimestamp() >= BlockChain.getInstance().getFeeValidationFixTimestamp())
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.rewardShareTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
else if (!(isRecipientAlsoMinter && existingRewardShareData == null))
|
||||
if (creator.getConfirmedBalance(Asset.QORT) < this.rewardShareTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
|
@@ -1,13 +1,7 @@
|
||||
package org.qortal.transaction;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -69,8 +63,8 @@ public abstract class Transaction {
|
||||
AT(21, false),
|
||||
CREATE_GROUP(22, true),
|
||||
UPDATE_GROUP(23, true),
|
||||
ADD_GROUP_ADMIN(24, false),
|
||||
REMOVE_GROUP_ADMIN(25, false),
|
||||
ADD_GROUP_ADMIN(24, true),
|
||||
REMOVE_GROUP_ADMIN(25, true),
|
||||
GROUP_BAN(26, false),
|
||||
CANCEL_GROUP_BAN(27, false),
|
||||
GROUP_KICK(28, false),
|
||||
@@ -250,6 +244,8 @@ public abstract class Transaction {
|
||||
INVALID_TIMESTAMP_SIGNATURE(95),
|
||||
ADDRESS_BLOCKED(96),
|
||||
NAME_BLOCKED(97),
|
||||
GROUP_APPROVAL_REQUIRED(98),
|
||||
ACCOUNT_NOT_TRANSFERABLE(99),
|
||||
INVALID_BUT_OK(999),
|
||||
NOT_YET_RELEASED(1000);
|
||||
|
||||
@@ -760,9 +756,13 @@ public abstract class Transaction {
|
||||
// Group no longer exists? Possibly due to blockchain orphaning undoing group creation?
|
||||
return true; // stops tx being included in block but it will eventually expire
|
||||
|
||||
String groupOwner = this.repository.getGroupRepository().getOwner(txGroupId);
|
||||
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||
|
||||
// If transaction's creator is group admin (of group with ID txGroupId) then auto-approve
|
||||
// This is disabled for null-owned groups, since these require approval from other admins
|
||||
PublicKeyAccount creator = this.getCreator();
|
||||
if (groupRepository.adminExists(txGroupId, creator.getAddress()))
|
||||
if (!groupOwnedByNullAccount && groupRepository.adminExists(txGroupId, creator.getAddress()))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
@@ -67,6 +67,11 @@ public class TransferPrivsTransaction extends Transaction {
|
||||
if (getSender().getConfirmedBalance(Asset.QORT) < this.transferPrivsTransactionData.getFee())
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
// Check sender doesn't have a blocksMintedPenalty, as these accounts cannot be transferred
|
||||
AccountData senderAccountData = this.repository.getAccountRepository().getAccount(getSender().getAddress());
|
||||
if (senderAccountData == null || senderAccountData.getBlocksMintedPenalty() != 0)
|
||||
return ValidationResult.ACCOUNT_NOT_TRANSFERABLE;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
|
@@ -103,7 +103,7 @@ public class UpdateGroupTransaction extends Transaction {
|
||||
Account newOwner = getNewOwner();
|
||||
|
||||
// Check new owner is not banned
|
||||
if (this.repository.getGroupRepository().banExists(this.updateGroupTransactionData.getGroupId(), newOwner.getAddress()))
|
||||
if (this.repository.getGroupRepository().banExists(this.updateGroupTransactionData.getGroupId(), newOwner.getAddress(), this.updateGroupTransactionData.getTimestamp()))
|
||||
return ValidationResult.BANNED_FROM_GROUP;
|
||||
|
||||
return ValidationResult.OK;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user