Compare commits

...

122 Commits

Author SHA1 Message Date
CalDescent
466c727dee Bump version to 3.8.9 2023-02-22 19:01:10 +00:00
CalDescent
ba9f3b335c Added unit test to reproduce the UPDATE_NAME issue and prove that the fix is working correctly. 2023-02-22 18:59:43 +00:00
CalDescent
148ca0af05 Fixed long term bug with UPDATE_NAME transactions, causing name data to be incorrectly deleted if newName == name. 2023-02-22 09:16:52 +00:00
CalDescent
c39b9c764b Bump version to 3.8.8 2023-02-20 18:12:40 +00:00
CalDescent
d30eb6141a Default minPeerVersion set to 3.8.7 2023-02-20 18:10:21 +00:00
CalDescent
52c806f9e6 Bump version to 3.8.7 2023-02-19 22:44:59 +00:00
CalDescent
b2d31a7e02 Rebuild the name's history before processing a CancelSellNameTransaction. 2023-02-19 22:26:22 +00:00
CalDescent
cfa0b1d8ea Bump version to 3.8.6 2023-02-19 18:02:22 +00:00
CalDescent
edacce1bac Improved logging when creating bootstraps, and catch/log all exceptions. 2023-02-19 17:43:13 +00:00
CalDescent
074cba2266 Added QCHAT_AUDIO and QCHAT_VOICE services (limited to 10MB each) 2023-02-19 17:33:17 +00:00
QuickMythril
f4a32d19dd Merge pull request #110 from QuickMythril/update-electrumx
Add new ElectrumX servers
2023-02-12 00:10:56 -05:00
QuickMythril
eb6d84c04d Add new ElectrumX servers 2023-02-12 00:10:13 -05:00
CalDescent
26587067d8 Merge pull request #109 from AlphaX-Projects/master
Add more data to active chats websocket
2023-02-11 13:25:13 +00:00
AlphaX-Projects
227d93a31e Merge branch 'Qortal:master' into master 2023-02-11 13:39:26 +01:00
CalDescent
830bae3dc1 Merge branch 'at-states-fix'
# Conflicts:
#	src/main/java/org/qortal/controller/repository/AtStatesPruner.java
#	src/main/java/org/qortal/controller/repository/AtStatesTrimmer.java
2023-02-10 17:45:22 +00:00
CalDescent
ec09312cc5 Updated AdvancedInstaller project for 3.8.5 2023-02-10 17:42:12 +00:00
AlphaX-Projects
11654ba9c6 Add Chat Data 2023-02-10 11:05:54 +01:00
AlphaX-Projects
ea356d1026 add signatures to websockt 2023-02-10 10:27:28 +01:00
CalDescent
e7a3e511bd Bump version to 3.8.5 2023-02-08 19:37:01 +00:00
CalDescent
6fca30ce75 Added GET /admin/summary/alltime endpoint, to view a summary of chain activity since genesis. 2023-02-07 19:56:54 +00:00
CalDescent
e903e59f7f Merge pull request #107 from QuickMythril/unused-address
Add unused foreign address to API calls
2023-02-06 18:43:22 +00:00
CalDescent
bef170df7e Updated PirateChain lightwallet servers. 2023-02-06 18:42:37 +00:00
QuickMythril
386bfa4e20 Merge pull request #108 from AlphaX-Projects/master
Add electrum servers and fix java reflect error
2023-02-05 07:38:30 -05:00
AlphaX-Projects
6f867031e2 Add electrum servers and fix java reflect error 2023-02-05 12:53:49 +01:00
QuickMythril
8f589391a6 Updated depreciated actions
Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: actions/checkout@v2, actions/cache@v2, actions/setup-java@v2. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/
2023-02-04 21:57:31 -05:00
QuickMythril
30c9f63cb1 Add unused foreign address to API calls
POST ​/crosschain​/{COIN}/unusedaddress
2023-02-04 21:03:55 -05:00
QuickMythril
952b21d9bd Merge pull request #105 from QuickMythril/update-electrumx
Updated ElectrumX servers
2023-02-04 18:40:39 -05:00
QuickMythril
1f410a503e Updated ElectrumX servers 2023-02-04 18:30:31 -05:00
CalDescent
f5e30eeaf5 Merge pull request #104 from QuickMythril/foreign-height
Add foreign chain height to API calls
2023-02-01 20:28:55 +00:00
QuickMythril
21f5d9a3d0 Add foreign chain height to API calls
GET ​/crosschain​/{COIN}/height
2023-01-31 17:23:25 -05:00
CalDescent
ab34fae810 Merge pull request #90 from QuickMythril/german
Updated German translations
2023-01-28 20:22:11 +00:00
CalDescent
42f2d015b7 Merge branch 'master' into german 2023-01-28 20:22:02 +00:00
CalDescent
2181ece28d Merge pull request #89 from lexandr0s/patch-2
Update ApiError_ru.properties
2023-01-28 20:21:13 +00:00
CalDescent
03a5d0e5f9 Merge pull request #88 from lexandr0s/patch-1
Update SysTray_ru.properties
2023-01-28 20:21:00 +00:00
CalDescent
352f094272 Merge pull request #99 from Nuc1eoN/polish-translation
Add polish translation
2023-01-28 20:20:35 +00:00
CalDescent
c5c826453b Removed unnecessary join when finding MESSAGE transactions, which caused secret to be unavailable when querying pruned blocks. 2023-01-28 15:41:48 +00:00
CalDescent
e86b9b1caf Added additional Litecoin ElectrumX server. 2023-01-28 15:34:30 +00:00
CalDescent
7fc170575c Merge branch 'cancel-sell-name-fixes' 2023-01-28 12:11:42 +00:00
CalDescent
876658256f Prevent a P2SH address being funded for a trade if there is an unconfirmed buy or cancel request in progress for it already.
This prevents foreign coins from leaving the local wallet when there is a high probability that the trade will fail, and therefore should reduce the chances of losing transaction fees due to refunds.

Whenever this occurs, the UI will show "Trade has an existing buy request or is pending cancellation." after clicking Buy.
2023-01-28 11:57:15 +00:00
CalDescent
a24ba40d5c Added additional Dogecoin ElectrumX server. 2023-01-28 09:54:15 +00:00
CalDescent
06d8a21714 Added CANCEL_SELL_NAME equivalents to NamesDatabaseIntegrityCheck.java 2023-01-27 19:38:26 +00:00
CalDescent
ae44065d7e Fixed issue with CancelSellName transactions. 2023-01-27 19:34:23 +00:00
CalDescent
6ad0989ea2 Reduce log spam 2023-01-27 18:35:44 +00:00
CalDescent
5962ebd08a More logging improvements in ArbitraryDataReader.decrypt() 2023-01-27 16:56:53 +00:00
CalDescent
bf06d47842 Create an ArbitraryDataResource object when building. Eventually this could be passed in to the reader instead of the individual components (service, name, identifier, etc)
This is now used to improve logging when extracting.
2023-01-27 16:55:43 +00:00
CalDescent
8c708558cb Implemented ElectrumX version negotiation. Fixes issues with DOGE wallet. 2023-01-27 14:33:34 +00:00
CalDescent
6b36d94c6f Removed searchResultsTransactions cache, to simplify code. The hostedTransactions cache is still in place, which limits disk reads when searching, so this additional cache isn't really needed. 2023-01-27 12:48:42 +00:00
CalDescent
1d568fa462 Return file lists via /arbitrary/metadata/* endpoints, but exclude it from /arbitrary/resources/* endpoints. 2023-01-22 16:29:23 +00:00
CalDescent
328ba48224 Merge branch 'master' into qdn-file-list 2023-01-22 16:12:54 +00:00
CalDescent
6196841609 Allow files without extensions in QCHAT_ATTACHMENT validation. 2023-01-22 15:59:16 +00:00
CalDescent
9f30571b12 Use a filename without an extension when publishing data from a string (instead of .tmp) 2023-01-22 15:58:53 +00:00
CalDescent
1f7fec6251 Exclude .qortal directory in validation functions, as it was incorrectly failing with "DIRECTORIES_NOT_ALLOWED". 2023-01-20 10:40:20 +00:00
CalDescent
c3f19ea0c1 Don't allow the custom validation methods to evade superclass validation. 2023-01-20 10:21:05 +00:00
CalDescent
e31515b4a2 Fixed bugs preventing single file GIF repositories and QCHAT attachments from passing validation. 2023-01-20 10:14:42 +00:00
CalDescent
8ad46b6344 Fixed/removed incorrect comments 2023-01-20 09:58:28 +00:00
CalDescent
2f7912abce Compute balances for Bitcoin-like coins using unspent outputs. Should fix occasional incorrect balance issue, and speed up loading time. 2023-01-18 19:30:43 +00:00
CalDescent
64529e8abf Added "reverse" and "includeOnlineSignatures" params to GET /blocks/range/{height} endpoint. 2023-01-18 19:04:54 +00:00
CalDescent
9d81ea7744 Bump version to 3.8.4 2023-01-16 20:26:00 +00:00
CalDescent
688acd466c Set checkpoint to block 1136300 2023-01-16 20:23:43 +00:00
CalDescent
81cf46f5dd Disable block signing on topOnly nodes. Minting rewards are still earned on topOnly for now. 2023-01-16 20:18:23 +00:00
CalDescent
4c52d6f0fc Fixed bug causing initial latestATStates data to be discarded. 2023-01-15 21:58:17 +00:00
CalDescent
de47a94677 Fixed bug causing initial latestATStates data to be discarded. 2023-01-15 15:51:10 +00:00
CalDescent
bd4c47dba6 Rework of AT state trimming and pruning, in order to more reliably track the "latest" AT states.
This should fix an edge case where AT states data was pruned/trimmed but it was then later required in consensus. The older state was deleted because it was replaced by a new "latest" state in a brand new block. But once the new "latest" state was orphaned from the block, the old "latest" state was then required again.

This works around the problem by excluding very recent blocks in the latest AT states data, so that it is unaffected by real-time sync activity.

The trade off is that we could end up retaining more AT states than needed, so a secondary cleanup process may need to run at some time in the future to remove these. But it should only be a minimal amount of data, and can be cleaned up with a single query. This would have been happening to a certain degree already.
2023-01-15 14:32:33 +00:00
CalDescent
c03f271825 Keep track of peers which are too divergent, and return an isTooDivergent boolean in /peers APIs.
isTooDivergent will be true or false if a definitive decision has been made, or missing from the response if not yet known. Therefore it should be safe to treat `"isTooDivergent": false` as a peer that is on the same chain.
2023-01-15 12:44:19 +00:00
CalDescent
dfe3754afc Block connections with peers older than 3.8.2, as those versions are nonfunctional due to recent feature triggers. 2023-01-15 12:07:27 +00:00
CalDescent
30105199a2 Default pruneBlockLimit increased from 1450 to 6000 (approx 5 days), to be more similar to the AT states retention time of full nodes. 2023-01-15 12:00:32 +00:00
CalDescent
e91e612b55 Added checkpoint lookup on startup.
Currently enabled for topOnly nodes only. This will detect if the node is on a divergent chain, and will force a bootstrap or resync (depending on settings) in order to rejoin the main chain.
2023-01-15 11:33:16 +00:00
CalDescent
2a55eba1f7 Updated AdvancedInstaller project for v3.8.3 2023-01-15 11:28:37 +00:00
CalDescent
39e59cbcf8 Bump version to 3.8.3 2023-01-14 18:47:46 +00:00
CalDescent
016191bdb0 Reduce log spam when a QDN resource can't be found due to it not being published. 2023-01-14 15:15:48 +00:00
CalDescent
0596a07c7d Reduced ArbitraryDataFileRequestThread count from 10 to 5, to reduce network flooding. 2023-01-14 12:58:35 +00:00
CalDescent
c62c59b445 Use correct timeout (12s) when sending arbitrary data to a peer, and improved logging. 2023-01-14 12:57:44 +00:00
CalDescent
f78101e9cc Updated a default bootstrap host to use a domain instead of its IP. 2023-01-14 11:07:54 +00:00
CalDescent
476fdcb31d Added serialization tests for chatReference, and grouped with other serialization tests into a single package. 2023-01-14 10:38:50 +00:00
CalDescent
02d5043ef7 Added missing calls to electrumX.setBlockchain(instance); for DGB and RVN. Thanks to @QuickMythril for noticing this. 2023-01-13 20:17:27 +00:00
CalDescent
0ad9e2f65b Added QCHAT_ATTACHMENT service, with custom validation function. 2023-01-13 20:08:47 +00:00
CalDescent
4dc0033a5a Added missing chatReferenceTimestamp in unit tests. 2023-01-13 19:45:52 +00:00
CalDescent
745cfe8ea1 chatReferenceTimestamp set to 1674316800000 (Sat, 21 Jan 2023 16:00:00 GMT) 2023-01-13 19:45:38 +00:00
CalDescent
6284a4691c Import test transactions as part of the serialization tests, to catch any issues with db schema data lengths. 2023-01-13 19:28:44 +00:00
CalDescent
41f88be55e Test serialization of CHAT transactions 2023-01-13 19:27:38 +00:00
CalDescent
ba95f8376f Increase CHAT transaction data limits to the maximum (4000 bytes) to allow for upcoming UI features. 2023-01-13 19:27:02 +00:00
CalDescent
8e97c05b56 Added missing feature trigger from unit tests. 2023-01-13 19:25:06 +00:00
CalDescent
eb569304ba Improved refund/refundAll HTLC code, to handle cases where there have been multiple purchase attempts for the same AT. 2023-01-06 10:38:25 +00:00
CalDescent
b0486f44bb Added chat_reference index to speed up searches. 2023-01-02 17:47:36 +00:00
CalDescent
cecf28ab7b Merge branch 'chat-reference'
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
#	src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
#	src/main/resources/blockchain.json
#	src/test/resources/test-chain-v2-block-timestamps.json
#	src/test/resources/test-chain-v2-disable-reference.json
#	src/test/resources/test-chain-v2-founder-rewards.json
#	src/test/resources/test-chain-v2-leftover-reward.json
#	src/test/resources/test-chain-v2-minting.json
#	src/test/resources/test-chain-v2-qora-holder-extremes.json
#	src/test/resources/test-chain-v2-qora-holder-reduction.json
#	src/test/resources/test-chain-v2-qora-holder.json
#	src/test/resources/test-chain-v2-reward-levels.json
#	src/test/resources/test-chain-v2-reward-scaling.json
#	src/test/resources/test-chain-v2-reward-shares.json
#	src/test/resources/test-chain-v2.json
2023-01-02 17:32:38 +00:00
CalDescent
98b92a5bf1 Introduced "historic threshold" to ARBITRARY transactions in order to save on verification times of older transactions.
This is based on the approach used for PUBLICIZE transactions.
2023-01-02 16:58:50 +00:00
CalDescent
6b45901c47 Fixed validation of existing reward share transactions. 2022-12-31 14:43:37 +00:00
CalDescent
166f9bd079 Bump version to 3.8.2 2022-12-24 21:28:02 +00:00
CalDescent
2f8f896077 Merge remote-tracking branch 'catbref/bugfix-deploy-at' 2022-12-24 16:01:23 +00:00
CalDescent
9a77aff0a6 Reduced difficulty of PUBLICIZE transactions from 15 to 14 (it is now the same as ARBITRARY transactions) 2022-12-24 14:10:49 +00:00
CalDescent
c6d65a88dc Increase mempow difficulty and threshold in ChatTransaction, to match the values in the UI. 2022-12-22 18:19:27 +00:00
CalDescent
4aea29a91b Improved PublicizeTransaction validation. 2022-12-22 18:03:29 +00:00
CalDescent
0e81665a36 Revert "Filter out peers of divergent or significantly inferior chains when syncing."
This reverts commit 1dc7f056f9. To be un-reverted in future when there is more time available for testing.
2022-12-22 15:10:19 +00:00
CalDescent
2a4ac1ed24 Limit to 250 CHAT messages per hour per account. 2022-12-22 15:09:04 +00:00
CalDescent
bb74b2d4f6 MAX_AVG_RESPONSE_TIME for ElectrumX servers increased from 0.5s to 1s. 2022-12-22 14:25:10 +00:00
CalDescent
758a02d71a Log Pirate light client server address if the wallet unable to be initialized. 2022-12-22 14:23:30 +00:00
CalDescent
7ae142fa64 Improved transaction validation. 2022-12-22 14:20:42 +00:00
CalDescent
a75ed0e634 Bump additional expandedAccount level references held in memory. 2022-12-22 14:18:39 +00:00
CalDescent
e40dc4af59 Fixed group ban expiry. 2022-12-22 14:16:57 +00:00
CalDescent
e678ea22e0 Fixed NPE in unit tests. Still need to work out how/when this was introduced. 2022-12-18 18:33:51 +00:00
CalDescent
cf3195cb83 Set "minAccountsToActivateShareBin" to 0 for certain tests. 2022-12-18 18:32:06 +00:00
CalDescent
80048208d1 Moved some test sponsorship utility methods to AccountUtils, so they can be used in other test classes too. 2022-12-15 12:14:42 +00:00
CalDescent
08de1fb4ec Disallow CHAT transactions with timestamps more than 5 minutes in the future. 2022-12-14 16:40:57 +00:00
CalDescent
99d5bf9103 Disallow transactions with timestamps more than 30 mins in the future (reduced from 24 hours) 2022-12-14 16:40:11 +00:00
CalDescent
1dc7f056f9 Filter out peers of divergent or significantly inferior chains when syncing. 2022-12-14 16:39:43 +00:00
CalDescent
cdeb2052b0 Bump version to 3.8.1 2022-12-08 18:26:34 +00:00
CalDescent
5c9109aca9 minPeerVersion set to 3.8.0 2022-12-08 18:25:19 +00:00
CalDescent
ccc1976d00 Added defensiveness 2022-12-08 18:25:03 +00:00
CalDescent
12fb6cd0ad onlineAccountMinterLevelValidationHeight moved forward to block 1092000 2022-12-08 18:24:34 +00:00
catbref
ae991dda4d Fix creatorPublicKey not being unmarshaled when calling POST /at to deploy an AT 2022-11-28 21:52:37 +00:00
CalDescent
2b6ae57a27 Merge branch 'master' into chat-reference
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
#	src/main/resources/blockchain.json
#	src/test/resources/test-chain-v2-block-timestamps.json
#	src/test/resources/test-chain-v2-disable-reference.json
#	src/test/resources/test-chain-v2-founder-rewards.json
#	src/test/resources/test-chain-v2-leftover-reward.json
#	src/test/resources/test-chain-v2-minting.json
#	src/test/resources/test-chain-v2-qora-holder-extremes.json
#	src/test/resources/test-chain-v2-qora-holder-reduction.json
#	src/test/resources/test-chain-v2-qora-holder.json
#	src/test/resources/test-chain-v2-reward-levels.json
#	src/test/resources/test-chain-v2-reward-scaling.json
#	src/test/resources/test-chain-v2-reward-shares.json
#	src/test/resources/test-chain-v2.json
2022-11-27 20:06:55 +00:00
CalDescent
055775b13d Include a list of files in the QDN metadata. 2022-10-30 18:54:38 +00:00
CalDescent
9d74f0eec0 Added haschatreference, with possible values of true, false, or null, to allow optional filtering by the presence or absense of a chat reference. 2022-10-24 19:21:29 +01:00
CalDescent
09014d07e0 Fixed issues retrieving chatReference from the db. 2022-10-23 19:29:31 +01:00
CalDescent
1dd039fb2d Merge branch 'master' into chat-reference 2022-10-23 14:14:23 +01:00
CalDescent
23a5c5f9b4 Fixed bug in original commit - we need to save the chat reference to the db. 2022-10-22 12:50:28 +01:00
CalDescent
a4759a0ef4 Re-ordered chat transaction transformation, to simplify UI code. New additions are now at the end of the data bytes. 2022-10-22 12:43:40 +01:00
CalDescent
910191b074 Added optional chatReference field to CHAT transactions.
This allows one message to reference another, e.g. for replies, edits, and reactions. We can't use the existing reference field as this is used for encryption and generally points to the user's lastReference at the time of signing.

"chatReference" is based on the "nameReference" field used in various name transactions, for similar purposes.

This needs a feature trigger timestamp to activate, and that same timestamp will need to be used in the UI since that is responsible for building the chat transactions.
2022-10-21 15:58:23 +01:00
Nuc1eoN
8ddf4c9f9f Add polish translation 2022-10-09 15:35:19 +02:00
QuickMythril
ff40b8f8ab Updated German translations 2022-06-23 01:43:33 -04:00
lexandr0s
c03344caae Update ApiError_ru.properties 2022-06-04 23:57:25 +04:00
lexandr0s
237b39a524 Update SysTray_ru.properties 2022-06-04 23:50:03 +04:00
118 changed files with 3001 additions and 461 deletions

View File

@@ -8,16 +8,16 @@ jobs:
mavenTesting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Cache local Maven repository
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Set up the Java JDK
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt'

View File

@@ -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:{ADE0C9E9-F7D9-4829-8626-8571C735C4D7} 1049:{F5230C0A-9D8C-4C70-AC72-17CECC8273B8} 2052:{D5A0760C-E5B3-4C4C-97B0-81CC445F07B9} 2057:{EF5EF0BE-0B00-4F5C-A2A0-DF2CB82FF20D} " Type="16"/>
<ROW Property="ProductCode" Value="1033:{CB85115E-ECCE-4B3D-BB7F-6251A2764922} 1049:{09AC1C62-4E33-4312-826A-38F597ED1B17} 2052:{3CF701B3-E118-4A31-A4B7-156CEA19FBCC} 2057:{468F337D-0EF8-41D1-B5DE-4EEE66BA2AF6} " Type="16"/>
<ROW Property="ProductLanguage" Value="2057"/>
<ROW Property="ProductName" Value="Qortal"/>
<ROW Property="ProductVersion" Value="3.6.3" Type="32"/>
<ROW Property="ProductVersion" Value="3.8.5" 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="{F4F774B9-18DC-4740-9552-EA16B98801C9}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_CustomARPName" ComponentId="{094B5D07-2258-4A39-9917-2E2F7F6E210B}" 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"/>

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>3.8.0</version>
<version>3.8.9</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>
@@ -304,6 +304,7 @@
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.qortal.controller.Controller</mainClass>
<manifestEntries>
<Multi-Release>true</Multi-Release>
<Class-Path>. ..</Class-Path>
</manifestEntries>
</transformer>

View File

@@ -52,6 +52,11 @@ public class SelfSponsorshipAlgoV1 {
public void run() throws DataException {
if (this.accountData == null) {
// Nothing to do
return;
}
this.fetchSponsorshipRewardShares();
if (this.sponsorshipRewardShares.isEmpty()) {
// Nothing to do

View File

@@ -1,6 +1,7 @@
package org.qortal.api.model;
import io.swagger.v3.oas.annotations.media.Schema;
import org.qortal.controller.Controller;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.network.Handshake;
@@ -36,6 +37,7 @@ public class ConnectedPeer {
public Long lastBlockTimestamp;
public UUID connectionId;
public String age;
public Boolean isTooDivergent;
protected ConnectedPeer() {
}
@@ -69,6 +71,11 @@ public class ConnectedPeer {
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);
}
}
}

View File

@@ -222,6 +222,42 @@ public class AdminResource {
}
}
@GET
@Path("/summary/alltime")
@Operation(
summary = "Summary of activity since genesis",
responses = {
@ApiResponse(
content = @Content(schema = @Schema(implementation = ActivitySummary.class))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public ActivitySummary allTimeSummary(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
Security.checkApiCallAllowed(request);
ActivitySummary summary = new ActivitySummary();
try (final Repository repository = RepositoryManager.getRepository()) {
int startHeight = 1;
long start = repository.getBlockRepository().fromHeight(startHeight).getTimestamp();
int endHeight = repository.getBlockRepository().getBlockchainHeight();
summary.setBlockCount(endHeight - startHeight);
summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
return summary;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/enginestats")
@Operation(

View File

@@ -719,7 +719,7 @@ public class ArbitraryResource {
try {
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false);
if (transactionMetadata != null) {
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, true);
if (resourceMetadata != null) {
return resourceMetadata;
}
@@ -1128,7 +1128,7 @@ public class ArbitraryResource {
if (path == null) {
// See if we have a string instead
if (string != null) {
File tempFile = File.createTempFile("qortal-", ".tmp");
File tempFile = File.createTempFile("qortal-", "");
tempFile.deleteOnExit();
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString()));
writer.write(string);
@@ -1138,7 +1138,7 @@ public class ArbitraryResource {
}
// ... or base64 encoded raw data
else if (base64 != null) {
File tempFile = File.createTempFile("qortal-", ".tmp");
File tempFile = File.createTempFile("qortal-", "");
tempFile.deleteOnExit();
Files.write(tempFile.toPath(), Base64.decode(base64));
path = tempFile.toPath().toString();
@@ -1288,7 +1288,7 @@ public class ArbitraryResource {
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
resourceInfo.service, resourceInfo.identifier);
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, false);
if (resourceMetadata != null) {
resourceInfo.metadata = resourceMetadata;
}

View File

@@ -634,13 +634,16 @@ public class BlocksResource {
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
ref = "count"
) @QueryParam("count") int count) {
public List<BlockData> getBlockRange(@PathParam("height") int height,
@Parameter(ref = "count") @QueryParam("count") int count,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockData> blocks = new ArrayList<>();
boolean shouldReverse = (reverse != null && reverse == true);
for (/* count already set */; count > 0; --count, ++height) {
int i = 0;
while (i < count) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
if (blockData == null) {
// Not found - try the archive
@@ -650,8 +653,14 @@ public class BlocksResource {
break;
}
}
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
blockData.setOnlineAccountsSignatures(null);
}
blocks.add(blockData);
height = shouldReverse ? height - 1 : height + 1;
i++;
}
return blocks;

View File

@@ -60,7 +60,7 @@ public class BootstrapResource {
bootstrap.validateBlockchain();
return bootstrap.create();
} catch (DataException | InterruptedException | IOException e) {
} catch (Exception e) {
LOGGER.info("Unable to create bootstrap", e);
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
}

View File

@@ -70,6 +70,8 @@ public class ChatResource {
@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) {
@@ -92,12 +94,18 @@ public class ChatResource {
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) {

View File

@@ -14,6 +14,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -35,6 +36,37 @@ public class CrossChainBitcoinResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Bitcoin block height",
description = "Returns the height of the most recent block in the Bitcoin chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getBitcoinHeight() {
Bitcoin bitcoin = Bitcoin.getInstance();
try {
Integer height = bitcoin.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -68,7 +100,7 @@ public class CrossChainBitcoinResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
Long balance = bitcoin.getWalletBalanceFromTransactions(key58);
Long balance = bitcoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
@@ -118,6 +150,45 @@ public class CrossChainBitcoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedBitcoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Bitcoin bitcoin = Bitcoin.getInstance();
if (!bitcoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return bitcoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -14,6 +14,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -35,6 +36,37 @@ public class CrossChainDigibyteResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Digibyte block height",
description = "Returns the height of the most recent block in the Digibyte chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getDigibyteHeight() {
Digibyte digibyte = Digibyte.getInstance();
try {
Integer height = digibyte.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -68,7 +100,7 @@ public class CrossChainDigibyteResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
Long balance = digibyte.getWalletBalanceFromTransactions(key58);
Long balance = digibyte.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
@@ -118,6 +150,45 @@ public class CrossChainDigibyteResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedDigibyteReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Digibyte digibyte = Digibyte.getInstance();
if (!digibyte.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return digibyte.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -21,6 +21,7 @@ import org.qortal.crosschain.SimpleTransaction;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -33,6 +34,37 @@ public class CrossChainDogecoinResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Dogecoin block height",
description = "Returns the height of the most recent block in the Dogecoin chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getDogecoinHeight() {
Dogecoin dogecoin = Dogecoin.getInstance();
try {
Integer height = dogecoin.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -66,7 +98,7 @@ public class CrossChainDogecoinResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
Long balance = dogecoin.getWalletBalanceFromTransactions(key58);
Long balance = dogecoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
@@ -116,6 +148,45 @@ public class CrossChainDogecoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedDogecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Dogecoin dogecoin = Dogecoin.getInstance();
if (!dogecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return dogecoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -8,11 +8,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
@@ -25,7 +24,6 @@ import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.qortal.api.*;
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
import org.qortal.controller.Controller;
import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
@@ -586,98 +584,103 @@ public class CrossChainHtlcResource {
}
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();
int lockTime = tradeBotData.getLockTimeA();
// Loop through all matching entries for this AT address, as there might be more than one
for (TradeBotData tradeBotData : tradeBotDataList) {
// 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);
if (tradeBotData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// 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);
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
int lockTime = tradeBotData.getLockTimeA();
// 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;
// We can't refund P2SH-A until lockTime-A has passed
if (NTP.getTime() <= lockTime * 1000L)
continue;
// 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));
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
int medianBlockTime = bitcoiny.getMedianBlockTime();
if (medianBlockTime <= lockTime)
continue;
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);
// 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;
case REDEEM_IN_PROGRESS:
case REDEEMED:
case REFUND_IN_PROGRESS:
case REFUNDED:
// Too late!
return false;
// 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 FUNDED:{
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
// Still waiting for P2SH-A to be funded...
continue;
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
// Pirate Chain custom integration
case REDEEM_IN_PROGRESS:
case REDEEMED:
case REFUND_IN_PROGRESS:
case REFUNDED:
// Too late!
continue;
PirateChain pirateChain = PirateChain.getInstance();
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA);
case FUNDED: {
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
// Get funding txid
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA);
if (fundingTxidHex == null) {
throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
// Pirate Chain custom integration
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);
}
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);
return true;
}
else {
// ElectrumX coins
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
// Validate the destination foreign blockchain address
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
bitcoiny.broadcastTransaction(p2shRefundTransaction);
}
return true;
}
}

View File

@@ -14,6 +14,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -35,6 +36,37 @@ public class CrossChainLitecoinResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Litecoin block height",
description = "Returns the height of the most recent block in the Litecoin chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getLitecoinHeight() {
Litecoin litecoin = Litecoin.getInstance();
try {
Integer height = litecoin.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -68,7 +100,7 @@ public class CrossChainLitecoinResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
Long balance = litecoin.getWalletBalanceFromTransactions(key58);
Long balance = litecoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
@@ -118,6 +150,45 @@ public class CrossChainLitecoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedLitecoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
if (!litecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return litecoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -20,6 +20,7 @@ import org.qortal.crosschain.SimpleTransaction;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -32,6 +33,37 @@ public class CrossChainPirateChainResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current PirateChain block height",
description = "Returns the height of the most recent block in the PirateChain chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getPirateChainHeight() {
PirateChain pirateChain = PirateChain.getInstance();
try {
Integer height = pirateChain.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(

View File

@@ -14,6 +14,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -35,6 +36,37 @@ public class CrossChainRavencoinResource {
@Context
HttpServletRequest request;
@GET
@Path("/height")
@Operation(
summary = "Returns current Ravencoin block height",
description = "Returns the height of the most recent block in the Ravencoin chain.",
responses = {
@ApiResponse(
content = @Content(
schema = @Schema(
type = "number"
)
)
)
}
)
@ApiErrors({ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
public String getRavencoinHeight() {
Ravencoin ravencoin = Ravencoin.getInstance();
try {
Integer height = ravencoin.getBlockchainHeight();
if (height == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return height.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/walletbalance")
@Operation(
@@ -68,7 +100,7 @@ public class CrossChainRavencoinResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
Long balance = ravencoin.getWalletBalanceFromTransactions(key58);
Long balance = ravencoin.getWalletBalance(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
@@ -118,6 +150,45 @@ public class CrossChainRavencoinResource {
}
}
@POST
@Path("/unusedaddress")
@Operation(
summary = "Returns first unused address for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getUnusedRavencoinReceiveAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Ravencoin ravencoin = Ravencoin.getInstance();
if (!ravencoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return ravencoin.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(

View File

@@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@@ -38,9 +39,12 @@ 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.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@@ -223,6 +227,17 @@ public class CrossChainTradeBotResource {
if (crossChainTradeData.mode != AcctMode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check if there is a buy or a cancel request in progress for this trade
List<Transaction.TransactionType> txTypes = List.of(Transaction.TransactionType.MESSAGE);
List<TransactionData> unconfirmed = repository.getTransactionRepository().getUnconfirmedTransactions(txTypes, null, 0, 0, false);
for (TransactionData transactionData : unconfirmed) {
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
if (Objects.equals(messageTransactionData.getRecipient(), atAddress)) {
// There is a pending request for this trade, so block this buy attempt to reduce the risk of refunds
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Trade has an existing buy request or is pending cancellation.");
}
}
AcctTradeBot.ResponseResult result = TradeBot.getInstance().startResponse(repository, atData, acct, crossChainTradeData,
tradeBotRespondRequest.foreignKey, tradeBotRespondRequest.receivingAddress);

View File

@@ -47,6 +47,8 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
txGroupId,
null,
null,
null,
null,
null, null, null);
sendMessages(session, chatMessages);
@@ -74,6 +76,8 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
null,
null,
null,
null,
null,
involvingAddresses,
null, null, null);

View File

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

View File

@@ -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;
@@ -59,6 +60,9 @@ public class ArbitraryDataReader {
private int layerCount;
private byte[] latestSignature;
// The resource being read
ArbitraryDataResource arbitraryDataResource = null;
public ArbitraryDataReader(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
// Ensure names are always lowercase
if (resourceIdType == ResourceIdType.NAME) {
@@ -115,6 +119,11 @@ public class ArbitraryDataReader {
return new ArbitraryDataBuildQueueItem(this.resourceId, this.resourceIdType, this.service, this.identifier);
}
private ArbitraryDataResource createArbitraryDataResource() {
return new ArbitraryDataResource(this.resourceId, this.resourceIdType, this.service, this.identifier);
}
/**
* loadAsynchronously
*
@@ -162,6 +171,8 @@ public class ArbitraryDataReader {
return;
}
this.arbitraryDataResource = this.createArbitraryDataResource();
this.preExecute();
this.deleteExistingFiles();
this.fetch();
@@ -169,10 +180,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();
@@ -427,7 +446,7 @@ public class ArbitraryDataReader {
byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null;
if (secret != null && secret.length == Transformer.AES256_LENGTH) {
try {
LOGGER.info("Decrypting using algorithm {}...", algorithm);
LOGGER.debug("Decrypting {} using algorithm {}...", this.arbitraryDataResource, algorithm);
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES");
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
@@ -438,7 +457,7 @@ public class ArbitraryDataReader {
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
| BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) {
LOGGER.info(String.format("Exception when decrypting using algorithm %s", algorithm), e);
LOGGER.info(String.format("Exception when decrypting %s using algorithm %s", this.arbitraryDataResource, algorithm), e);
throw new DataException(String.format("Unable to decrypt file at path %s using algorithm %s: %s", this.filePath, algorithm, e.getMessage()));
}
} else {

View File

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

View File

@@ -23,16 +23,13 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.*;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ArbitraryDataWriter {
@@ -50,6 +47,7 @@ public class ArbitraryDataWriter {
private final String description;
private final List<String> tags;
private final Category category;
private List<String> files;
private int chunkSize = ArbitraryDataFile.CHUNK_SIZE;
@@ -80,12 +78,14 @@ public class ArbitraryDataWriter {
this.description = ArbitraryDataTransactionMetadata.limitDescription(description);
this.tags = ArbitraryDataTransactionMetadata.limitTags(tags);
this.category = category;
this.files = new ArrayList<>(); // Populated in buildFileList()
}
public void save() throws IOException, DataException, InterruptedException, MissingDataException {
try {
this.preExecute();
this.validateService();
this.buildFileList();
this.process();
this.compress();
this.encrypt();
@@ -143,6 +143,24 @@ public class ArbitraryDataWriter {
}
}
private void buildFileList() throws IOException {
// Single file resources consist of a single element in the file list
boolean isSingleFile = this.filePath.toFile().isFile();
if (isSingleFile) {
this.files.add(this.filePath.getFileName().toString());
return;
}
// Multi file resources require a walk through the directory tree
try (Stream<Path> stream = Files.walk(this.filePath)) {
this.files = stream
.filter(Files::isRegularFile)
.map(p -> this.filePath.relativize(p).toString())
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}
private void process() throws DataException, IOException, MissingDataException {
switch (this.method) {
@@ -285,6 +303,7 @@ public class ArbitraryDataWriter {
metadata.setTags(this.tags);
metadata.setCategory(this.category);
metadata.setChunks(this.arbitraryDataFile.chunkHashList());
metadata.setFiles(this.files);
metadata.write();
// Create an ArbitraryDataFile from the JSON file (we don't have a signature yet)

View File

@@ -0,0 +1,22 @@
package org.qortal.arbitrary.exception;
import org.qortal.repository.DataException;
public class DataNotPublishedException extends DataException {
public DataNotPublishedException() {
}
public DataNotPublishedException(String message) {
super(message);
}
public DataNotPublishedException(String message, Throwable cause) {
super(message, cause);
}
public DataNotPublishedException(Throwable cause) {
super(cause);
}
}

View File

@@ -19,6 +19,7 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
private String description;
private List<String> tags;
private Category category;
private List<String> files;
private static int MAX_TITLE_LENGTH = 80;
private static int MAX_DESCRIPTION_LENGTH = 500;
@@ -77,6 +78,20 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
}
this.chunks = chunksList;
}
List<String> filesList = new ArrayList<>();
if (metadata.has("files")) {
JSONArray files = metadata.getJSONArray("files");
if (files != null) {
for (int i=0; i<files.length(); i++) {
String tag = files.getString(i);
if (tag != null) {
filesList.add(tag);
}
}
}
this.files = filesList;
}
}
@Override
@@ -111,6 +126,14 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
}
outer.put("chunks", chunks);
JSONArray files = new JSONArray();
if (this.files != null) {
for (String file : this.files) {
files.put(file);
}
}
outer.put("files", files);
this.jsonString = outer.toString(2);
LOGGER.trace("Transaction metadata: {}", this.jsonString);
}
@@ -156,6 +179,14 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
return this.category;
}
public void setFiles(List<String> files) {
this.files = files;
}
public List<String> getFiles() {
return this.files;
}
public boolean containsChunk(byte[] chunk) {
for (byte[] c : this.chunks) {
if (Arrays.equals(c, chunk)) {

View File

@@ -10,9 +10,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
@@ -20,9 +18,52 @@ 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) throws IOException {
ValidationResult superclassResult = super.validate(path);
if (superclassResult != ValidationResult.OK) {
return superclassResult;
}
// Custom validation function to require a single file, with a whitelisted extension
int fileCount = 0;
File[] files = path.toFile().listFiles();
// If already a single file, replace the list with one that contains that file only
if (files == null && path.toFile().isFile()) {
files = new File[] { path.toFile() };
}
if (files != null) {
for (File file : files) {
if (file.getName().equals(".qortal")) {
continue;
}
if (file.isDirectory()) {
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
}
final String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
// We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
final List<String> allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
if (extension == null || !allowedExtensions.contains(extension)) {
return ValidationResult.INVALID_FILE_EXTENSION;
}
fileCount++;
}
}
if (fileCount != 1) {
return ValidationResult.INVALID_FILE_COUNT;
}
return ValidationResult.OK;
}
},
WEBSITE(200, true, null, null) {
@Override
public ValidationResult validate(Path path) {
public ValidationResult validate(Path path) throws IOException {
ValidationResult superclassResult = super.validate(path);
if (superclassResult != ValidationResult.OK) {
return superclassResult;
}
// Custom validation function to require an index HTML file in the root directory
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
String[] files = path.toFile().list();
@@ -43,6 +84,8 @@ public enum Service {
QCHAT_IMAGE(420, true, 500*1024L, null),
VIDEO(500, false, null, null),
AUDIO(600, false, null, null),
QCHAT_AUDIO(610, true, 10*1024*1024L, null),
QCHAT_VOICE(620, true, 10*1024*1024L, null),
BLOG(700, false, null, null),
BLOG_POST(777, false, null, null),
BLOG_COMMENT(778, false, null, null),
@@ -53,12 +96,24 @@ public enum Service {
METADATA(1100, false, null, null),
GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
@Override
public ValidationResult validate(Path path) {
public ValidationResult validate(Path path) throws IOException {
ValidationResult superclassResult = super.validate(path);
if (superclassResult != ValidationResult.OK) {
return superclassResult;
}
// Custom validation function to require .gif files only, and at least 1
int gifCount = 0;
File[] files = path.toFile().listFiles();
// If already a single file, replace the list with one that contains that file only
if (files == null && path.toFile().isFile()) {
files = new File[] { path.toFile() };
}
if (files != null) {
for (File file : files) {
if (file.getName().equals(".qortal")) {
continue;
}
if (file.isDirectory()) {
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
}
@@ -143,7 +198,8 @@ public enum Service {
MISSING_INDEX_FILE(4),
DIRECTORIES_NOT_ALLOWED(5),
INVALID_FILE_EXTENSION(6),
MISSING_DATA(7);
MISSING_DATA(7),
INVALID_FILE_COUNT(8);
public final int value;

View File

@@ -1522,6 +1522,9 @@ 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)
@@ -1535,6 +1538,7 @@ public class Block {
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()));
}
@@ -1542,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 {

View File

@@ -76,7 +76,9 @@ public class BlockChain {
disableReferenceTimestamp,
increaseOnlineAccountsDifficultyTimestamp,
onlineAccountMinterLevelValidationHeight,
selfSponsorshipAlgoV1Height;
selfSponsorshipAlgoV1Height,
feeValidationFixTimestamp,
chatReferenceTimestamp;
}
// Custom transaction fees
@@ -98,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;
@@ -379,6 +388,10 @@ public class BlockChain {
return this.oneNamePerAccount;
}
public List<Checkpoint> getCheckpoints() {
return this.checkpoints;
}
public List<RewardByHeight> getBlockRewardsByHeight() {
return this.rewardsByHeight;
}
@@ -501,6 +514,14 @@ public class BlockChain {
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
@@ -669,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;
@@ -689,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()));
}
}
@@ -713,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);

View File

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

View File

@@ -769,6 +769,16 @@ public class Controller extends Thread {
}
};
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();

View File

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

View File

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

View File

@@ -48,7 +48,6 @@ public class ArbitraryDataStorageManager extends Thread {
private List<ArbitraryTransactionData> hostedTransactions;
private String searchQuery;
private List<ArbitraryTransactionData> searchResultsTransactions;
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
@@ -344,11 +343,6 @@ public class ArbitraryDataStorageManager extends Thread {
*/
public List<ArbitraryTransactionData> searchHostedTransactions(Repository repository, String query, Integer limit, Integer offset) {
// Load from results cache if we can (results that exists for the same query), to avoid disk reads
if (this.searchResultsTransactions != null && this.searchQuery.equals(query.toLowerCase())) {
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
}
// Using cache if we can, to avoid disk reads
if (this.hostedTransactions == null) {
this.hostedTransactions = this.loadAllHostedTransactions(repository);
@@ -376,10 +370,7 @@ public class ArbitraryDataStorageManager extends Thread {
// Sort by newest first
searchResultsList.sort(Comparator.comparingLong(ArbitraryTransactionData::getTimestamp).reversed());
// Update cache
this.searchResultsTransactions = searchResultsList;
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
return ArbitraryTransactionUtils.limitOffsetTransactions(searchResultsList, limit, offset);
}
/**

View File

@@ -39,9 +39,11 @@ public class AtStatesPruner implements Runnable {
try (final Repository repository = RepositoryManager.getRepository()) {
int pruneStartHeight = repository.getATRepository().getAtPruneHeight();
int maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
repository.discardChanges();
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
repository.saveChanges();
while (!Controller.isStopping()) {
repository.discardChanges();
@@ -91,7 +93,8 @@ public class AtStatesPruner implements Runnable {
if (upperPrunableHeight > upperBatchHeight) {
pruneStartHeight = upperBatchHeight;
repository.getATRepository().setAtPruneHeight(pruneStartHeight);
repository.getATRepository().rebuildLatestAtStates();
maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
repository.saveChanges();
final int finalPruneStartHeight = pruneStartHeight;

View File

@@ -26,9 +26,11 @@ public class AtStatesTrimmer implements Runnable {
try (final Repository repository = RepositoryManager.getRepository()) {
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
int maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
repository.discardChanges();
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
repository.saveChanges();
while (!Controller.isStopping()) {
repository.discardChanges();
@@ -69,7 +71,8 @@ public class AtStatesTrimmer implements Runnable {
if (upperTrimmableHeight > upperBatchHeight) {
trimStartHeight = upperBatchHeight;
repository.getATRepository().setAtTrimHeight(trimStartHeight);
repository.getATRepository().rebuildLatestAtStates();
maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
repository.saveChanges();
final int finalTrimStartHeight = trimStartHeight;

View File

@@ -102,6 +102,21 @@ public class NamesDatabaseIntegrityCheck {
}
}
// Process CANCEL_SELL_NAME transactions
if (currentTransaction.getType() == TransactionType.CANCEL_SELL_NAME) {
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) currentTransaction;
Name nameObj = new Name(repository, cancelSellNameTransactionData.getName());
if (nameObj != null && nameObj.getNameData() != null) {
nameObj.cancelSell(cancelSellNameTransactionData);
modificationCount++;
LOGGER.trace("Processed CANCEL_SELL_NAME transaction for name {}", name);
}
else {
// Something went wrong
throw new DataException(String.format("Name data not found for name %s", cancelSellNameTransactionData.getName()));
}
}
// Process BUY_NAME transactions
if (currentTransaction.getType() == TransactionType.BUY_NAME) {
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
@@ -128,7 +143,7 @@ public class NamesDatabaseIntegrityCheck {
public int rebuildAllNames() {
int modificationCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
List<String> names = this.fetchAllNames(repository);
List<String> names = this.fetchAllNames(repository); // TODO: de-duplicate, to speed up this process
for (String name : names) {
modificationCount += this.rebuildName(name, repository);
}
@@ -326,6 +341,10 @@ public class NamesDatabaseIntegrityCheck {
TransactionType.BUY_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
signatures.addAll(buyNameTransactions);
List<byte[]> cancelSellNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
TransactionType.CANCEL_SELL_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
signatures.addAll(cancelSellNameTransactions);
List<TransactionData> transactions = new ArrayList<>();
for (byte[] signature : signatures) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
@@ -390,6 +409,12 @@ public class NamesDatabaseIntegrityCheck {
names.add(sellNameTransactionData.getName());
}
}
if ((transactionData instanceof CancelSellNameTransactionData)) {
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData;
if (!names.contains(cancelSellNameTransactionData.getName())) {
names.add(cancelSellNameTransactionData.getName());
}
}
}
return names;
}

View File

@@ -157,4 +157,18 @@ public class PruneManager {
return (height < latestUnprunedHeight);
}
/**
* When rebuilding the latest AT states, we need to specify a maxHeight, so that we aren't tracking
* very recent AT states that could potentially be orphaned. This method ensures that AT states
* are given a sufficient number of blocks to confirm before being tracked as a latest AT state.
*/
public static int getMaxHeightForLatestAtStates(Repository repository) throws DataException {
// Get current chain height, and subtract a certain number of "confirmation" blocks
// This is to ensure we are basing our latest AT states data on confirmed blocks -
// ones that won't be orphaned in any normal circumstances
final int confirmationBlocks = 250;
final int chainHeight = repository.getBlockRepository().getBlockchainHeight();
return chainHeight - confirmationBlocks;
}
}

View File

@@ -49,6 +49,7 @@ public class Bitcoin extends Bitcoiny {
//CLOSED new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrum.pabu.io", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrumx.dev", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("electrumx.hodlwallet.com", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("gd42.org", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("korea.electrum-server.com", Server.ConnectionType.SSL, 50002),
@@ -56,28 +57,75 @@ public class Bitcoin extends Bitcoiny {
//1.15.0 new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
//1.15.0 new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002),
//1.14.0 new Server("electrum.coinext.com.br", Server.ConnectionType.SSL, 50002),
//F1.7.0 new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
new Server("104.248.139.211", Server.ConnectionType.SSL, 50002),
new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
new Server("142.93.6.38", Server.ConnectionType.SSL, 50002),
new Server("157.245.172.236", Server.ConnectionType.SSL, 50002),
new Server("167.172.226.175", Server.ConnectionType.SSL, 50002),
new Server("167.172.42.31", Server.ConnectionType.SSL, 50002),
new Server("178.62.80.20", Server.ConnectionType.SSL, 50002),
new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
new Server("188.165.206.215", Server.ConnectionType.SSL, 50002),
new Server("188.165.211.112", Server.ConnectionType.SSL, 50002),
new Server("2azzarita.hopto.org", Server.ConnectionType.SSL, 50002),
new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022),
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
new Server("65.39.140.37", Server.ConnectionType.SSL, 50002),
new Server("68.183.188.105", Server.ConnectionType.SSL, 50002),
new Server("71.73.14.254", Server.ConnectionType.SSL, 50002),
new Server("94.23.247.135", Server.ConnectionType.SSL, 50002),
new Server("assuredly.not.fyi", Server.ConnectionType.SSL, 50002),
new Server("ax101.blockeng.ch", Server.ConnectionType.SSL, 50002),
new Server("ax102.blockeng.ch", Server.ConnectionType.SSL, 50002),
new Server("b.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("b6.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.dermichi.com", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lu.ke", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002),
new Server("blkhub.net", Server.ConnectionType.SSL, 50002),
new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
new Server("btc.ocf.sh", Server.ConnectionType.SSL, 50002),
new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002),
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.bhoovd.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx.dev", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitcoinlizard.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002),
new Server("electrum.exan.tech", Server.ConnectionType.SSL, 50002),
new Server("electrum.kendigisland.xyz", Server.ConnectionType.SSL, 50002),
new Server("electrum.mmitech.info", Server.ConnectionType.SSL, 50002),
new Server("electrum.petrkr.net", Server.ConnectionType.SSL, 50002),
new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002),
new Server("electrum.thomasfischbach.de", Server.ConnectionType.SSL, 50002),
new Server("electrum0.snel.it", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx-core.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002),
new Server("ex03.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("ex05.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("ex07.axalgo.com", Server.ConnectionType.SSL, 50002),
new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002),
new Server("fulcrum.grey.pw", Server.ConnectionType.SSL, 50002),
new Server("fulcrum.sethforprivacy.com", Server.ConnectionType.SSL, 51002),
new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002),
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002),
new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002),
new Server("osr1ex1.compumundohipermegared.one", Server.ConnectionType.SSL, 50002),
new Server("smmalis37.ddns.net", Server.ConnectionType.SSL, 50002),
new Server("ulrichard.ch", Server.ConnectionType.SSL, 50002),
new Server("vmd104012.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd104014.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("vmd84592.contaboserver.net", Server.ConnectionType.SSL, 50002),
new Server("xtrum.com", Server.ConnectionType.SSL, 50002));
}

View File

@@ -167,6 +167,16 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return blockTimestamps.get(5);
}
/**
* Returns height from latest block.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
public int getBlockchainHeight() throws ForeignBlockchainException {
int height = this.blockchainProvider.getCurrentHeight();
return height;
}
/** Returns fee per transaction KB. To be overridden for testnet/regtest. */
public Coin getFeePerKb() {
return this.bitcoinjContext.getFeePerKb();
@@ -357,19 +367,33 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @return unspent BTC balance, or null if unable to determine balance
*/
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
// It's more accurate to calculate the balance from the transactions, rather than asking Bitcoinj
return this.getWalletBalanceFromTransactions(key58);
Long balance = 0L;
// Context.propagate(bitcoinjContext);
//
// Wallet wallet = walletFromDeterministicKey58(key58);
// wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
//
// Coin balance = wallet.getBalance();
// if (balance == null)
// return null;
//
// return balance.value;
List<TransactionOutput> allUnspentOutputs = new ArrayList<>();
Set<String> walletAddresses = this.getWalletAddresses(key58);
for (String address : walletAddresses) {
allUnspentOutputs.addAll(this.getUnspentOutputs(address));
}
for (TransactionOutput output : allUnspentOutputs) {
if (!output.isAvailableForSpending()) {
continue;
}
balance += output.getValue().value;
}
return balance;
}
public Long getWalletBalanceFromBitcoinj(String key58) {
Context.propagate(bitcoinjContext);
Wallet wallet = walletFromDeterministicKey58(key58);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
Coin balance = wallet.getBalance();
if (balance == null)
return null;
return balance.value;
}
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
@@ -464,6 +488,64 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
}
public Set<String> getWalletAddresses(String key58) throws ForeignBlockchainException {
synchronized (this) {
Context.propagate(bitcoinjContext);
Wallet wallet = walletFromDeterministicKey58(key58);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
keyChain.maybeLookAhead();
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
Set<String> keySet = new HashSet<>();
int unusedCounter = 0;
int ki = 0;
do {
boolean areAllKeysUnused = true;
for (; ki < keys.size(); ++ki) {
DeterministicKey dKey = keys.get(ki);
// Check for transactions
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
keySet.add(address.toString());
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
areAllKeysUnused = false;
}
}
if (areAllKeysUnused) {
// No transactions
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
// ... and we've hit our search limit
break;
}
// We haven't hit our search limit yet so increment the counter and keep looking
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
} else {
// Some keys in this batch were used, so reset the counter
unusedCounter = 0;
}
// Generate some more keys
keys.addAll(generateMoreKeys(keyChain));
// Process new keys
} while (true);
return keySet;
}
}
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
long amount = 0;
long total = 0L;

View File

@@ -45,6 +45,9 @@ public class Digibyte extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 55002),
new Server("electrum-dgb.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1-dgb.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20059),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20059),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20059));
@@ -134,6 +137,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;

View File

@@ -45,10 +45,13 @@ public class Dogecoin extends Bitcoiny {
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 54002),
new Server("electrum-doge.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1-doge.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20060),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20060),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20060));
// TODO: add more mainnet servers. It's too centralized.
}
@Override

View File

@@ -5,6 +5,7 @@ import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.text.DecimalFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,7 +31,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
private static final Random RANDOM = new Random();
// See: https://electrumx.readthedocs.io/en/latest/protocol-changes.html
private static final double MIN_PROTOCOL_VERSION = 1.2;
private static final double MAX_PROTOCOL_VERSION = 2.0; // Higher than current latest, for hopeful future-proofing
private static final String CLIENT_NAME = "Qortal";
private static final int BLOCK_HEADER_LENGTH = 80;
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
@@ -40,7 +45,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;
@@ -679,6 +684,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
this.scanner = new Scanner(this.socket.getInputStream());
this.scanner.useDelimiter("\n");
// All connections need to start with a version negotiation
this.connectedRpc("server.version");
// Check connection is suitable by asking for server features, including genesis block hash
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
@@ -725,6 +733,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
JSONArray requestParams = new JSONArray();
requestParams.addAll(Arrays.asList(params));
// server.version needs additional params to negotiate a version
if (method.equals("server.version")) {
requestParams.add(CLIENT_NAME);
List<String> versions = new ArrayList<>();
DecimalFormat df = new DecimalFormat("#.#");
versions.add(df.format(MIN_PROTOCOL_VERSION));
versions.add(df.format(MAX_PROTOCOL_VERSION));
requestParams.add(versions);
}
requestJson.put("params", requestParams);
String request = requestJson.toJSONString() + "\n";

View File

@@ -45,15 +45,19 @@ public class Litecoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc
//CLOSED new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
//CLOSED new Server("electrum-ltc.someguy123.net", Server.ConnectionType.SSL, 50002),
//CLOSED new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
//BEHIND new Server("62.171.169.176", Server.ConnectionType.SSL, 50002),
//PHISHY new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002),
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443),
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
new Server("electrum-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002));
}

View File

@@ -57,9 +57,9 @@ public class PirateChain extends Bitcoiny {
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("arrrlightd.qortal.online", ConnectionType.SSL, 443),
new Server("arrrlightd1.qortal.online", ConnectionType.SSL, 443),
new Server("arrrlightd2.qortal.online", ConnectionType.SSL, 443),
new Server("wallet-arrr1.qortal.online", ConnectionType.SSL, 443),
new Server("wallet-arrr2.qortal.online", ConnectionType.SSL, 443),
new Server("wallet-arrr3.qortal.online", ConnectionType.SSL, 443),
new Server("lightd.pirate.black", ConnectionType.SSL, 443));
}

View File

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

View File

@@ -45,13 +45,17 @@ public class Ravencoin extends Bitcoiny {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
new Server("aethyn.com", ConnectionType.SSL, 50002),
new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
new Server("rvn-dashboard.com", ConnectionType.SSL, 50002),
new Server("rvn4lyfe.com", ConnectionType.SSL, 50002),
//CLOSED new Server("aethyn.com", ConnectionType.SSL, 50002),
//CLOSED new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
//BEHIND new Server("electrum3.rvn.rocks", ConnectionType.SSL, 50002),
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 56002),
new Server("electrum-rvn.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1-rvn.qortal.online", ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20051),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20051),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20051));
new Server("electrum3.cipig.net", ConnectionType.SSL, 20051),
new Server("rvn-dashboard.com", ConnectionType.SSL, 50002),
new Server("rvn4lyfe.com", ConnectionType.SSL, 50002));
}
@Override
@@ -138,6 +142,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;

View File

@@ -15,22 +15,24 @@ public class ArbitraryResourceMetadata {
private List<String> tags;
private Category category;
private String categoryName;
private List<String> files;
public ArbitraryResourceMetadata() {
}
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category) {
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category, List<String> files) {
this.title = title;
this.description = description;
this.tags = tags;
this.category = category;
this.files = files;
if (category != null) {
this.categoryName = category.getName();
}
}
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata) {
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata, boolean includeFileList) {
if (transactionMetadata == null) {
return null;
}
@@ -39,10 +41,20 @@ public class ArbitraryResourceMetadata {
List<String> tags = transactionMetadata.getTags();
Category category = transactionMetadata.getCategory();
if (title == null && description == null && tags == null && category == null) {
// We don't always want to include the file list as it can be too verbose
List<String> files = null;
if (includeFileList) {
files = transactionMetadata.getFiles();
}
if (title == null && description == null && tags == null && category == null && files == null) {
return null;
}
return new ArbitraryResourceMetadata(title, description, tags, category);
return new ArbitraryResourceMetadata(title, description, tags, category, files);
}
public List<String> getFiles() {
return this.files;
}
}

View File

@@ -17,17 +17,21 @@ public class ActiveChats {
private Long timestamp;
private String sender;
private String senderName;
private byte[] signature;
private byte[] data;
protected GroupChat() {
/* JAXB */
}
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName) {
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName, byte[] signature, byte[] data) {
this.groupId = groupId;
this.groupName = groupName;
this.timestamp = timestamp;
this.sender = sender;
this.senderName = senderName;
this.signature = signature;
this.data = data;
}
public int getGroupId() {
@@ -49,6 +53,14 @@ public class ActiveChats {
public String getSenderName() {
return this.senderName;
}
public byte[] getSignature() {
return this.signature;
}
public byte[] getData() {
return this.data;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
@@ -118,4 +130,4 @@ public class ActiveChats {
return this.direct;
}
}
}

View File

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

View File

@@ -3,6 +3,7 @@ package org.qortal.data.transaction;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlTransient;
import org.qortal.transaction.Transaction.TransactionType;
@@ -19,6 +20,11 @@ public class CancelSellNameTransactionData extends TransactionData {
@Schema(description = "which name to cancel selling", example = "my-name")
private String name;
// For internal use when orphaning
@XmlTransient
@Schema(hidden = true)
private Long salePrice;
// Constructors
// For JAXB
@@ -30,11 +36,17 @@ public class CancelSellNameTransactionData extends TransactionData {
this.creatorPublicKey = this.ownerPublicKey;
}
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name) {
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name, Long salePrice) {
super(TransactionType.CANCEL_SELL_NAME, baseTransactionData);
this.ownerPublicKey = baseTransactionData.creatorPublicKey;
this.name = name;
this.salePrice = salePrice;
}
/** From network/API */
public CancelSellNameTransactionData(BaseTransactionData baseTransactionData, String name) {
this(baseTransactionData, name, null);
}
// Getters / setters
@@ -47,4 +59,12 @@ public class CancelSellNameTransactionData extends TransactionData {
return this.name;
}
public Long getSalePrice() {
return this.salePrice;
}
public void setSalePrice(Long salePrice) {
this.salePrice = salePrice;
}
}

View File

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

View File

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

View File

@@ -16,6 +16,8 @@ import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Unicode;
import java.util.Objects;
public class Name {
// Properties
@@ -116,7 +118,7 @@ public class Name {
this.repository.getNameRepository().save(this.nameData);
if (!updateNameTransactionData.getNewName().isEmpty())
if (!updateNameTransactionData.getNewName().isEmpty() && !Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName()))
// Name has changed, delete old entry
this.repository.getNameRepository().delete(updateNameTransactionData.getNewName());
@@ -180,8 +182,12 @@ public class Name {
}
public void cancelSell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException {
// Mark not for-sale but leave price in case we want to orphan
// Update previous sale price in transaction data
cancelSellNameTransactionData.setSalePrice(this.nameData.getSalePrice());
// Mark not for-sale
this.nameData.setIsForSale(false);
this.nameData.setSalePrice(null);
// Save sale info into repository
this.repository.getNameRepository().save(this.nameData);
@@ -190,6 +196,7 @@ public class Name {
public void uncancelSell(CancelSellNameTransactionData cancelSellNameTransactionData) throws DataException {
// Mark as for-sale using existing price
this.nameData.setIsForSale(true);
this.nameData.setSalePrice(cancelSellNameTransactionData.getSalePrice());
// Save no-sale info into repository
this.repository.getNameRepository().save(this.nameData);

View File

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

View File

@@ -339,7 +339,7 @@ public class Network {
try {
if (!isConnected) {
// Add this signature to the list of pending requests for this peer
LOGGER.info("Making connection to peer {} to request files for signature {}...", peerAddressString, Base58.encode(signature));
LOGGER.debug("Making connection to peer {} to request files for signature {}...", peerAddressString, Base58.encode(signature));
Peer peer = new Peer(peerData);
peer.setIsDataPeer(true);
peer.addPendingSignatureRequest(signature);

View File

@@ -155,6 +155,11 @@ public class Peer {
*/
private CommonBlockData commonBlockData;
/**
* Last time we detected this peer as TOO_DIVERGENT
*/
private Long lastTooDivergentTime;
// Message stats
private static class MessageStats {
@@ -383,6 +388,14 @@ public class Peer {
this.commonBlockData = commonBlockData;
}
public Long getLastTooDivergentTime() {
return this.lastTooDivergentTime;
}
public void setLastTooDivergentTime(Long lastTooDivergentTime) {
this.lastTooDivergentTime = lastTooDivergentTime;
}
public boolean isSyncInProgress() {
return this.syncInProgress;
}

View File

@@ -119,7 +119,7 @@ public interface ATRepository {
* <p>
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
*/
public void rebuildLatestAtStates() throws DataException;
public void rebuildLatestAtStates(int maxHeight) throws DataException;
/** Returns height of first trimmable AT state. */

View File

@@ -279,7 +279,9 @@ public class Bootstrap {
LOGGER.info("Generating checksum file...");
String checksum = Crypto.digestHexString(compressedOutputPath.toFile(), 1024*1024);
LOGGER.info("checksum: {}", checksum);
Path checksumPath = Paths.get(String.format("%s.sha256", compressedOutputPath.toString()));
LOGGER.info("Writing checksum to path: {}", checksumPath);
Files.writeString(checksumPath, checksum, StandardOpenOption.CREATE);
// Return the path to the compressed bootstrap file

View File

@@ -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, byte[] reference, 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;

View File

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

View File

@@ -603,7 +603,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public void rebuildLatestAtStates() throws DataException {
public void rebuildLatestAtStates(int maxHeight) throws DataException {
// latestATStatesLock is to prevent concurrent updates on LatestATStates
// that could result in one process using a partial or empty dataset
// because it was in the process of being rebuilt by another thread
@@ -624,11 +624,12 @@ public class HSQLDBATRepository implements ATRepository {
+ "CROSS JOIN LATERAL("
+ "SELECT height FROM ATStates "
+ "WHERE ATStates.AT_address = ATs.AT_address "
+ "AND height <= ?"
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
+ ") "
+ ")";
try {
this.repository.executeCheckedUpdate(insertSql);
this.repository.executeCheckedUpdate(insertSql, maxHeight);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);

View File

@@ -24,8 +24,8 @@ public class HSQLDBChatRepository implements ChatRepository {
@Override
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes,
List<String> involving, Integer limit, Integer offset, Boolean reverse)
throws DataException {
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 "
@@ -62,6 +62,18 @@ public class HSQLDBChatRepository implements ChatRepository {
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");
@@ -103,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());
@@ -141,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);
}
@@ -163,11 +177,11 @@ public class HSQLDBChatRepository implements ChatRepository {
private List<GroupChat> getActiveGroupChats(String address) throws DataException {
// Find groups where address is a member and potential latest message details
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name "
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name, signature, data "
+ "FROM GroupMembers "
+ "JOIN Groups USING (group_id) "
+ "LEFT OUTER JOIN LATERAL("
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name "
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name, signature, data "
+ "FROM ChatTransactions "
+ "JOIN Transactions USING (signature) "
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
@@ -191,8 +205,10 @@ public class HSQLDBChatRepository implements ChatRepository {
String sender = resultSet.getString(4);
String senderName = resultSet.getString(5);
byte[] signature = resultSet.getBytes(6);
byte[] data = resultSet.getBytes(7);
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName);
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName, signature, data);
groupChats.add(groupChat);
} while (resultSet.next());
}
@@ -201,7 +217,7 @@ public class HSQLDBChatRepository implements ChatRepository {
}
// We need different SQL to handle group-less chat
String grouplessSql = "SELECT created_when, sender, SenderNames.name "
String grouplessSql = "SELECT created_when, sender, SenderNames.name, signature, data "
+ "FROM ChatTransactions "
+ "JOIN Transactions USING (signature) "
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
@@ -214,15 +230,19 @@ public class HSQLDBChatRepository implements ChatRepository {
Long timestamp = null;
String sender = null;
String senderName = null;
byte[] signature = null;
byte[] data = null;
if (resultSet != null) {
// We found a recipient-less, group-less CHAT message, so report its details
timestamp = resultSet.getLong(1);
sender = resultSet.getString(2);
senderName = resultSet.getString(3);
signature = resultSet.getBytes(4);
data = resultSet.getBytes(5);
}
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName);
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName, signature, data);
groupChats.add(groupChat);
} catch (SQLException e) {
throw new DataException("Unable to fetch active group chats from repository", e);
@@ -277,4 +297,4 @@ public class HSQLDBChatRepository implements ChatRepository {
return directChats;
}
}
}

View File

@@ -99,7 +99,7 @@ public class HSQLDBDatabasePruning {
// It's essential that we rebuild the latest AT states here, as we are using this data in the next query.
// Failing to do this will result in important AT states being deleted, rendering the database unusable.
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(endHeight);
// Loop through all the LatestATStates and copy them to the new table

View File

@@ -980,6 +980,19 @@ public class HSQLDBDatabaseUpdates {
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;
case 46:
// We need to track the sale price when canceling a name sale, so it can be put back when orphaned
stmt.execute("ALTER TABLE CancelSellNameTransactions ADD sale_price QortalAmount");
break;
default:
// nothing to do
return false;

View File

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

View File

@@ -28,7 +28,6 @@ public class HSQLDBMessageRepository implements MessageRepository {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT signature from MessageTransactions "
+ "JOIN Transactions USING (signature) "
+ "JOIN BlockTransactions ON transaction_signature = signature "
+ "WHERE ");
List<String> whereClauses = new ArrayList<>();

View File

@@ -17,15 +17,16 @@ public class HSQLDBCancelSellNameTransactionRepository extends HSQLDBTransaction
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT name FROM CancelSellNameTransactions WHERE signature = ?";
String sql = "SELECT name, sale_price FROM CancelSellNameTransactions WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
return null;
String name = resultSet.getString(1);
Long salePrice = resultSet.getLong(2);
return new CancelSellNameTransactionData(baseTransactionData, name);
return new CancelSellNameTransactionData(baseTransactionData, name, salePrice);
} catch (SQLException e) {
throw new DataException("Unable to fetch cancel sell name transaction from repository", e);
}
@@ -38,7 +39,7 @@ public class HSQLDBCancelSellNameTransactionRepository extends HSQLDBTransaction
HSQLDBSaver saveHelper = new HSQLDBSaver("CancelSellNameTransactions");
saveHelper.bind("signature", cancelSellNameTransactionData.getSignature()).bind("owner", cancelSellNameTransactionData.getOwnerPublicKey()).bind("name",
cancelSellNameTransactionData.getName());
cancelSellNameTransactionData.getName()).bind("sale_price", cancelSellNameTransactionData.getSalePrice());
try {
saveHelper.execute(this.repository);

View File

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

View File

@@ -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
@@ -209,7 +215,7 @@ public class Settings {
public long recoveryModeTimeout = 10 * 60 * 1000L;
/** Minimum peer version number required in order to sync with them */
private String minPeerVersion = "3.7.0";
private String minPeerVersion = "3.8.7";
/** Whether to allow connections with peers below minPeerVersion
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
* If false, sync will be blocked both ways, and they will not appear in the peers list */
@@ -267,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
@@ -640,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;
}

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import java.util.List;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.CancelSellNameTransactionData;
import org.qortal.data.transaction.TransactionData;
@@ -81,7 +82,13 @@ public class CancelSellNameTransaction extends Transaction {
@Override
public void preProcess() throws DataException {
// Nothing to do
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData;
// Rebuild this name in the Names table from the transaction history
// This is necessary because in some rare cases names can be missing from the Names table after registration
// but we have been unable to reproduce the issue and track down the root cause
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
namesDatabaseIntegrityCheck.rebuildName(cancelSellNameTransactionData.getName(), this.repository);
}
@Override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.ChatTransactionData;
@@ -22,11 +23,13 @@ public class ChatTransactionTransformer extends TransactionTransformer {
private static final int NONCE_LENGTH = INT_LENGTH;
private static final int HAS_RECIPIENT_LENGTH = BOOLEAN_LENGTH;
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
private static final int HAS_CHAT_REFERENCE_LENGTH = BOOLEAN_LENGTH;
private static final int CHAT_REFERENCE_LENGTH = SIGNATURE_LENGTH;
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
private static final int IS_TEXT_LENGTH = BOOLEAN_LENGTH;
private static final int IS_ENCRYPTED_LENGTH = BOOLEAN_LENGTH;
private static final int EXTRAS_LENGTH = NONCE_LENGTH + HAS_RECIPIENT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH;
private static final int EXTRAS_LENGTH = NONCE_LENGTH + HAS_RECIPIENT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH + HAS_CHAT_REFERENCE_LENGTH;
protected static final TransactionLayout layout;
@@ -77,13 +80,24 @@ public class ChatTransactionTransformer extends TransactionTransformer {
long fee = byteBuffer.getLong();
byte[] chatReference = null;
if (timestamp >= BlockChain.getInstance().getChatReferenceTimestamp()) {
boolean hasChatReference = byteBuffer.get() != 0;
if (hasChatReference) {
chatReference = new byte[CHAT_REFERENCE_LENGTH];
byteBuffer.get(chatReference);
}
}
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
String sender = Crypto.toAddress(senderPublicKey);
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, data, isText, isEncrypted);
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, chatReference, data, isText, isEncrypted);
}
public static int getDataLength(TransactionData transactionData) {
@@ -94,6 +108,9 @@ public class ChatTransactionTransformer extends TransactionTransformer {
if (chatTransactionData.getRecipient() != null)
dataLength += RECIPIENT_LENGTH;
if (chatTransactionData.getChatReference() != null)
dataLength += CHAT_REFERENCE_LENGTH;
return dataLength;
}
@@ -124,6 +141,16 @@ public class ChatTransactionTransformer extends TransactionTransformer {
bytes.write(Longs.toByteArray(chatTransactionData.getFee()));
if (transactionData.getTimestamp() >= BlockChain.getInstance().getChatReferenceTimestamp()) {
// Include chat reference if it's not null
if (chatTransactionData.getChatReference() != null) {
bytes.write((byte) 1);
bytes.write(chatTransactionData.getChatReference());
} else {
bytes.write((byte) 0);
}
}
if (chatTransactionData.getSignature() != null)
bytes.write(chatTransactionData.getSignature());

View File

@@ -82,9 +82,14 @@
"transactionV6Timestamp": 9999999999999,
"disableReferenceTimestamp": 1655222400000,
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
"onlineAccountMinterLevelValidationHeight": 1093400,
"selfSponsorshipAlgoV1Height": 1092400
"onlineAccountMinterLevelValidationHeight": 1092000,
"selfSponsorshipAlgoV1Height": 1092400,
"feeValidationFixTimestamp": 1671918000000,
"chatReferenceTimestamp": 1674316800000
},
"checkpoints": [
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
],
"genesisInfo": {
"version": 4,
"timestamp": "1593450000000",

View File

@@ -0,0 +1,83 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# Keys are from api.ApiError enum
# "localeLang": "pl",
### Common ###
JSON = nie udało się przetworzyć wiadomości JSON
INSUFFICIENT_BALANCE = niedostateczne środki
UNAUTHORIZED = nieautoryzowane połączenie API
REPOSITORY_ISSUE = błąd repozytorium
NON_PRODUCTION = to wywołanie API nie jest dozwolone dla systemów produkcyjnych
BLOCKCHAIN_NEEDS_SYNC = blockchain musi się najpierw zsynchronizować
NO_TIME_SYNC = zegar się jeszcze nie zsynchronizował
### Validation ###
INVALID_SIGNATURE = nieprawidłowa sygnatura
INVALID_ADDRESS = nieprawidłowy adres
INVALID_PUBLIC_KEY = nieprawidłowy klucz publiczny
INVALID_DATA = nieprawidłowe dane
INVALID_NETWORK_ADDRESS = nieprawidłowy adres sieci
ADDRESS_UNKNOWN = nieznany adres konta
INVALID_CRITERIA = nieprawidłowe kryteria wyszukiwania
INVALID_REFERENCE = nieprawidłowe skierowanie
TRANSFORMATION_ERROR = nie udało się przekształcić JSON w transakcję
INVALID_PRIVATE_KEY = klucz prywatny jest niepoprawny
INVALID_HEIGHT = nieprawidłowa wysokość bloku
CANNOT_MINT = konto nie możne bić monet
### Blocks ###
BLOCK_UNKNOWN = blok nieznany
### Transactions ###
TRANSACTION_UNKNOWN = nieznana transakcja
PUBLIC_KEY_NOT_FOUND = nie znaleziono klucza publicznego
# this one is special in that caller expected to pass two additional strings, hence the two %s
TRANSACTION_INVALID = transakcja nieważna: %s (%s)
### Naming ###
NAME_UNKNOWN = nazwa nieznana
### Asset ###
INVALID_ASSET_ID = nieprawidłowy identyfikator aktywy
INVALID_ORDER_ID = nieprawidłowy identyfikator zlecenia aktywy
ORDER_UNKNOWN = nieznany identyfikator zlecenia aktywy
### Groups ###
GROUP_UNKNOWN = nieznana grupa
### Foreign Blockchain ###
FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = obcy blockchain lub problem z siecią ElectrumX
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = niewystarczające środki na obcym blockchainie
FOREIGN_BLOCKCHAIN_TOO_SOON = zbyt wczesne nadawanie transakcji na obcym blockchainie (okres karencji/średni czas bloku)
### Trade Portal ###
ORDER_SIZE_TOO_SMALL = zbyt niska kwota zlecenia
### Data ###
FILE_NOT_FOUND = plik nie został znaleziony
NO_REPLY = peer nie odpowiedział w wyznaczonym czasie

View File

@@ -16,7 +16,7 @@ NON_PRODUCTION = этот вызов API не разрешен для произ
BLOCKCHAIN_NEEDS_SYNC = блокчейн должен сначала синхронизироваться
NO_TIME_SYNC = пока нет синхронизации часов
NO_TIME_SYNC = время не синхронизировано
### Validation ###
INVALID_SIGNATURE = недействительная подпись
@@ -72,7 +72,7 @@ FOREIGN_BLOCKCHAIN_NETWORK_ISSUE = проблема с внешним блокч
FOREIGN_BLOCKCHAIN_BALANCE_ISSUE = недостаточный баланс на внешнем блокчейне
FOREIGN_BLOCKCHAIN_TOO_SOON = слишком рано для трансляции транзакции во внений блокчей (время блокировки/среднее время блока)
FOREIGN_BLOCKCHAIN_TOO_SOON = слишком рано для трансляции транзакции во внешний блокчей (время блокировки/среднее время блока)
### Trade Portal ###
ORDER_SIZE_TOO_SMALL = слишком маленькая сумма ордера
@@ -80,4 +80,4 @@ ORDER_SIZE_TOO_SMALL = слишком маленькая сумма ордера
### Data ###
FILE_NOT_FOUND = файл не найден
NO_REPLY = узел не ответил данными
NO_REPLY = нет ответа

View File

@@ -5,11 +5,11 @@ APPLYING_UPDATE_AND_RESTARTING = Automatisches Update anwenden und neu starten
AUTO_UPDATE = Automatisches Update
BLOCK_HEIGHT = height
BLOCK_HEIGHT = Blockhöhe
BLOCKS_REMAINING = blocks remaining
BUILD_VERSION = Build-Version
BUILD_VERSION = Entwicklungs-Version
CHECK_TIME_ACCURACY = Prüfe Zeitgenauigkeit
@@ -23,7 +23,7 @@ CREATING_BACKUP_OF_DB_FILES = Erstellen Backup von Datenbank Dateien …
DB_BACKUP = Datenbank Backup
DB_CHECKPOINT = Datenbank Kontrollpunkt
DB_CHECKPOINT = Datenbank Check
DB_MAINTENANCE = Datenbank Instandhaltung
@@ -31,18 +31,18 @@ EXIT = Verlassen
LITE_NODE = Lite node
MINTING_DISABLED = NOT minting
MINTING_DISABLED = Kein minting
MINTING_ENABLED = \u2714 Minting
MINTING_ENABLED = \u2714 Minting aktiviert
OPEN_UI = Öffne UI
PERFORMING_DB_CHECKPOINT = Speichern nicht übergebener Datenbank Änderungen
PERFORMING_DB_CHECKPOINT = Speichern von unbestätigten Datenbankänderungen...
PERFORMING_DB_MAINTENANCE = Planmäßige Wartung durchführen...
SYNCHRONIZE_CLOCK = Synchronisiere Uhr
SYNCHRONIZING_BLOCKCHAIN = Synchronisierung
SYNCHRONIZING_BLOCKCHAIN = Synchronisierung der Blockchain
SYNCHRONIZING_CLOCK = Synchronisierung Uhr
SYNCHRONIZING_CLOCK = Synchronisierung der Uhr

View File

@@ -0,0 +1,46 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
APPLYING_UPDATE_AND_RESTARTING = Zastosowanie automatycznej aktualizacji i ponowne uruchomienie...
AUTO_UPDATE = Automatyczna aktualizacja
BLOCK_HEIGHT = wysokość
BUILD_VERSION = Wersja kompilacji
CHECK_TIME_ACCURACY = Sprawdz dokładność czasu
CONNECTING = Łączenie
CONNECTION = połączenie
CONNECTIONS = połączenia
CREATING_BACKUP_OF_DB_FILES = Tworzenie kopii zapasowej plików bazy danych...
DB_BACKUP = Kopia zapasowa bazy danych
DB_CHECKPOINT = Punkt kontrolny bazy danych...
DB_MAINTENANCE = Konserwacja bazy danych
EXIT = Zakończ
LITE_NODE = Lite node
MINTING_DISABLED = Mennica zamknięta
MINTING_ENABLED = \u2714 Mennica aktywna
OPEN_UI = Otwórz interfejs użytkownika
PERFORMING_DB_CHECKPOINT = Zapisywanie niezaksięgowanych zmian w bazie danych...
PERFORMING_DB_MAINTENANCE = Performing scheduled maintenance...
SYNCHRONIZE_CLOCK = Synchronizuj zegar
SYNCHRONIZING_BLOCKCHAIN = Synchronizacja
SYNCHRONIZING_CLOCK = Synchronizacja zegara

View File

@@ -1,7 +1,7 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
APPLYING_UPDATE_AND_RESTARTING = Применение автоматического обновления и перезапуска...
APPLYING_UPDATE_AND_RESTARTING = Применение автоматического обновления и перезапуск...
AUTO_UPDATE = Автоматическое обновление

View File

@@ -0,0 +1,195 @@
#
ACCOUNT_ALREADY_EXISTS = Account existiert bereits
ACCOUNT_CANNOT_REWARD_SHARE = Account kann keine Belohnung teilen
ADDRESS_ABOVE_RATE_LIMIT = address hat das angegebene Geschwindigkeitlimit erreicht
ADDRESS_BLOCKED = Addresse ist geblockt
ALREADY_GROUP_ADMIN = bereits Gruppen Admin
ALREADY_GROUP_MEMBER = bereits Gruppen Mitglied
ALREADY_VOTED_FOR_THAT_OPTION = bereits für diese Option gestimmt
ASSET_ALREADY_EXISTS = asset existiert bereits
ASSET_DOES_NOT_EXIST = asset nicht gefunden
ASSET_DOES_NOT_MATCH_AT = asset passt nicht mit AT's asset
ASSET_NOT_SPENDABLE = asset ist nicht ausgabefähig
AT_ALREADY_EXISTS = AT existiert bereits
AT_IS_FINISHED = AT ist fertig
AT_UNKNOWN = AT unbekannt
BAN_EXISTS = ban besteht bereits
BAN_UNKNOWN = ban unbekannt
BANNED_FROM_GROUP = von der gruppe gebannt
BUYER_ALREADY_OWNER = Käufer ist bereits Besitzer
CLOCK_NOT_SYNCED = Uhr nicht synchronisiert
DUPLICATE_MESSAGE = Adresse sendete doppelte Nachricht
DUPLICATE_OPTION = Duplizierungsmöglichkeit
GROUP_ALREADY_EXISTS = Gruppe besteht bereits
GROUP_APPROVAL_DECIDED = Gruppenfreigabe bereits beschlossen
GROUP_APPROVAL_NOT_REQUIRED = Gruppenfreigabe nicht erforderlich
GROUP_DOES_NOT_EXIST = Gruppe nicht vorhanden
GROUP_ID_MISMATCH = Gruppen-ID stimmt nicht überein
GROUP_OWNER_CANNOT_LEAVE = Gruppenbesitzer kann Gruppe nicht verlassen
HAVE_EQUALS_WANT = das bessesene-asset ist das selbe wie das gesuchte-asset
INCORRECT_NONCE = falsche PoW-Nonce
INSUFFICIENT_FEE = unzureichende Gebühr
INVALID_ADDRESS = ungültige Adresse
INVALID_AMOUNT = ungültiger Betrag
INVALID_ASSET_OWNER = Ungültiger Eigentümer
INVALID_AT_TRANSACTION = ungültige AT-Transaktion
INVALID_AT_TYPE_LENGTH = ungültige AT 'Typ' Länge
INVALID_BUT_OK = ungültig aber OK
INVALID_CREATION_BYTES = ungültige Erstellungs der bytes
INVALID_DATA_LENGTH = ungültige Datenlänge
INVALID_DESCRIPTION_LENGTH = ungültige Länge der Beschreibung
INVALID_GROUP_APPROVAL_THRESHOLD = ungültiger Schwellenwert für die Gruppenzulassung
INVALID_GROUP_BLOCK_DELAY = Ungültige Blockverzögerung der Gruppenfreigabe
INVALID_GROUP_ID = ungültige Gruppen-ID
INVALID_GROUP_OWNER = ungültiger Gruppenbesitzer
INVALID_LIFETIME = unzulässige Lebensdauer
INVALID_NAME_LENGTH = ungültige Namenslänge
INVALID_NAME_OWNER = ungültiger Besitzername
INVALID_OPTION_LENGTH = ungültige Länge der Optionen
INVALID_OPTIONS_COUNT = Anzahl ungültiger Optionen
INVALID_ORDER_CREATOR = ungültiger Auftragsersteller
INVALID_PAYMENTS_COUNT = Anzahl ungültiger Zahlungen
INVALID_PUBLIC_KEY = ungültiger öffentlicher Schlüssel
INVALID_QUANTITY = unzulässige Menge
INVALID_REFERENCE = ungültige Referenz
INVALID_RETURN = ungültige Rückgabe
INVALID_REWARD_SHARE_PERCENT = ungültig Prozent der Belohnunganteile
INVALID_SELLER = unzulässiger Verkäufer
INVALID_TAGS_LENGTH = ungültige 'tags'-Länge
INVALID_TIMESTAMP_SIGNATURE = Ungültige Zeitstempel-Signatur
INVALID_TX_GROUP_ID = Ungültige Transaktionsgruppen-ID
INVALID_VALUE_LENGTH = ungültige 'Wert'-Länge
INVITE_UNKNOWN = Gruppeneinladung unbekannt
JOIN_REQUEST_EXISTS = Gruppeneinladung existiert bereits
MAXIMUM_REWARD_SHARES = die maximale Anzahl von Reward-Shares für dieses Konto erreicht
MISSING_CREATOR = fehlender Ersteller
MULTIPLE_NAMES_FORBIDDEN = mehrere registrierte Namen pro Konto sind untersagt
NAME_ALREADY_FOR_SALE = Name bereits zum Verkauf
NAME_ALREADY_REGISTERED = Name bereits registriert
NAME_BLOCKED = Name geblockt
NAME_DOES_NOT_EXIST = Name nicht vorhanden
NAME_NOT_FOR_SALE = Name ist unverkäuflich
NAME_NOT_NORMALIZED = Name nicht in Unicode-'normalisierter' Form
NEGATIVE_AMOUNT = ungültiger/negativer Betrag
NEGATIVE_FEE = ungültige/negative Gebühr
NEGATIVE_PRICE = ungültiger/negativer Preis
NO_BALANCE = unzureichendes Guthaben
NO_BLOCKCHAIN_LOCK = die Blockchain des Knotens ist beschäftigt
NO_FLAG_PERMISSION = Konto hat diese Berechtigung nicht
NOT_GROUP_ADMIN = Account ist kein Gruppenadmin
NOT_GROUP_MEMBER = Account kein Gruppenmitglied
NOT_MINTING_ACCOUNT = Account kann nicht minten
NOT_YET_RELEASED = Funktion noch nicht freigegeben
OK = OK
ORDER_ALREADY_CLOSED = Asset Trade Order ist bereits geschlossen
ORDER_DOES_NOT_EXIST = asset trade order existiert nicht
POLL_ALREADY_EXISTS = Umfrage bereits vorhanden
POLL_DOES_NOT_EXIST = Umfrage nicht vorhanden
POLL_OPTION_DOES_NOT_EXIST = Umfrageoption existiert nicht
PUBLIC_KEY_UNKNOWN = öffentlicher Schlüssel unbekannt
REWARD_SHARE_UNKNOWN = Geteilte Belohnungen unbekant
SELF_SHARE_EXISTS = Selbstbeteiligung (Geteilte Belohnungen) sind breits vorhanden
TIMESTAMP_TOO_NEW = Zeitstempel zu neu
TIMESTAMP_TOO_OLD = Zeitstempel zu alt
TOO_MANY_UNCONFIRMED = Account hat zu viele unbestätigte Transaktionen am laufen
TRANSACTION_ALREADY_CONFIRMED = Transaktionen sind bereits bestätigt
TRANSACTION_ALREADY_EXISTS = Transaktionen existiert bereits
TRANSACTION_UNKNOWN = Unbekante Transaktion
TX_GROUP_ID_MISMATCH = Transaktion Gruppen ID stimmt nicht überein

View File

@@ -0,0 +1,196 @@
#
ACCOUNT_ALREADY_EXISTS = konto już istnieje
ACCOUNT_CANNOT_REWARD_SHARE = konto nie może udostępniać nagród
ADDRESS_ABOVE_RATE_LIMIT = adres osiągnął określony limit stawki
ADDRESS_BLOCKED = ten adres jest zablokowany
ALREADY_GROUP_ADMIN = już adminem grupy
ALREADY_GROUP_MEMBER = już członkiem grupy
ALREADY_VOTED_FOR_THAT_OPTION = już zagłosowano na ta opcje
ASSET_ALREADY_EXISTS = aktywa już istnieje
ASSET_DOES_NOT_EXIST = aktywa nie istnieje
ASSET_DOES_NOT_MATCH_AT = aktywa nie pasuje do aktywy AT
ASSET_NOT_SPENDABLE = aktywa nie jest rozporządzalna
AT_ALREADY_EXISTS = AT już istnieje
AT_IS_FINISHED = AT zakończył
AT_UNKNOWN = AT nieznany
BAN_EXISTS = ban już istnieje
BAN_UNKNOWN = ban nieznany
BANNED_FROM_GROUP = zbanowany z grupy
BUYER_ALREADY_OWNER = kupca jest już właścicielem
CLOCK_NOT_SYNCED = zegar nie zsynchronizowany
DUPLICATE_MESSAGE = adres wysłał duplikat wiadomości
DUPLICATE_OPTION = duplikat opcji
GROUP_ALREADY_EXISTS = grupa już istnieje
GROUP_APPROVAL_DECIDED = zatwierdzenie grupy już zdecydowano
GROUP_APPROVAL_NOT_REQUIRED = zatwierdzenie grupy nie jest wymagane
GROUP_DOES_NOT_EXIST = grupa nie istnieje
GROUP_ID_MISMATCH = niedopasowanie identyfikatora grupy
GROUP_OWNER_CANNOT_LEAVE = właściciel grupy nie może opuścić grupy
HAVE_EQUALS_WANT = posiadana aktywa równa się chcianej aktywie
INCORRECT_NONCE = nieprawidłowy nonce PoW
INSUFFICIENT_FEE = niewystarczająca opłata
INVALID_ADDRESS = nieprawidłowy adres
INVALID_AMOUNT = nieprawidłowa kwota
INVALID_ASSET_OWNER = nieprawidłowy właściciel aktywów
INVALID_AT_TRANSACTION = nieważna transakcja AT
INVALID_AT_TYPE_LENGTH = nieprawidłowa długość typu AT
INVALID_BUT_OK = nieważne, ale OK
INVALID_CREATION_BYTES = nieprawidłowe bajty tworzenia
INVALID_DATA_LENGTH = nieprawidłowa długość danych
INVALID_DESCRIPTION_LENGTH = nieprawidłowa długość opisu
INVALID_GROUP_APPROVAL_THRESHOLD = nieprawidłowy próg zatwierdzenia grupy
INVALID_GROUP_BLOCK_DELAY = nieprawidłowe opóźnienie bloku zatwierdzenia grupy
INVALID_GROUP_ID = nieprawidłowy identyfikator grupy
INVALID_GROUP_OWNER = nieprawidłowy właściciel grupy
INVALID_LIFETIME = nieprawidłowy czas istnienia
INVALID_NAME_LENGTH = nieprawidłowa długość nazwy
INVALID_NAME_OWNER = nieprawidłowy właściciel nazwy
INVALID_OPTION_LENGTH = nieprawidłowa długość opcji
INVALID_OPTIONS_COUNT = nieprawidłowa liczba opcji
INVALID_ORDER_CREATOR = nieprawidłowy twórca zlecenia
INVALID_PAYMENTS_COUNT = nieprawidłowa liczba płatności
INVALID_PUBLIC_KEY = nieprawidłowy klucz publiczny
INVALID_QUANTITY = nieprawidłowa ilość
INVALID_REFERENCE = nieprawidłowe skierowanie
INVALID_RETURN = nieprawidłowy zwrot
INVALID_REWARD_SHARE_PERCENT = nieprawidłowy procent udziału w nagrodzie
INVALID_SELLER = nieprawidłowy sprzedawca
INVALID_TAGS_LENGTH = nieprawidłowa długość tagów
INVALID_TIMESTAMP_SIGNATURE = nieprawidłowa sygnatura znacznika czasu
INVALID_TX_GROUP_ID = nieprawidłowy identyfikator grupy transakcji
INVALID_VALUE_LENGTH = nieprawidłowa długość wartości
INVITE_UNKNOWN = zaproszenie do grupy nieznane
JOIN_REQUEST_EXISTS = wniosek o dołączenie do grupy już istnieje
MAXIMUM_REWARD_SHARES = osiągnięto już maksymalną liczbę udziałów w nagrodzie dla tego konta
MISSING_CREATOR = brak twórcy
MULTIPLE_NAMES_FORBIDDEN = zabronione jest używanie wielu nazw na jednym koncie
NAME_ALREADY_FOR_SALE = nazwa już wystawiona na sprzedaż
NAME_ALREADY_REGISTERED = nazwa już zarejestrowana
NAME_BLOCKED = ta nazwa jest zablokowana
NAME_DOES_NOT_EXIST = nazwa nie istnieje
NAME_NOT_FOR_SALE = nazwa nie jest przeznaczona do sprzedaży
NAME_NOT_NORMALIZED = nazwa nie jest w formie 'znormalizowanej' Unicode
NEGATIVE_AMOUNT = nieprawidłowa/ujemna kwota
NEGATIVE_FEE = nieprawidłowa/ujemna opłata
NEGATIVE_PRICE = nieprawidłowa/ujemna cena
NO_BALANCE = niewystarczające środki
NO_BLOCKCHAIN_LOCK = węzeł blockchain jest obecnie zajęty
NO_FLAG_PERMISSION = konto nie ma tego uprawnienia
NOT_GROUP_ADMIN = konto nie jest adminem grupy
NOT_GROUP_MEMBER = konto nie jest członkiem grupy
NOT_MINTING_ACCOUNT = konto nie może bić monet
NOT_YET_RELEASED = funkcja nie została jeszcze udostępniona
OK = OK
ORDER_ALREADY_CLOSED = zlecenie handlu aktywami jest już zakończone
ORDER_DOES_NOT_EXIST = zlecenie sprzedaży aktywów nie istnieje
POLL_ALREADY_EXISTS = ankieta już istnieje
POLL_DOES_NOT_EXIST = ankieta nie istnieje
POLL_OPTION_DOES_NOT_EXIST = opcja ankiety nie istnieje
PUBLIC_KEY_UNKNOWN = klucz publiczny nieznany
REWARD_SHARE_UNKNOWN = nieznany udział w nagrodzie
SELF_SHARE_EXISTS = samoudział (udział w nagrodzie) już istnieje
TIMESTAMP_TOO_NEW = zbyt nowy znacznik czasu
TIMESTAMP_TOO_OLD = zbyt stary znacznik czasu
TOO_MANY_UNCONFIRMED = rachunek ma zbyt wiele niepotwierdzonych transakcji w toku
TRANSACTION_ALREADY_CONFIRMED = transakcja została już potwierdzona
TRANSACTION_ALREADY_EXISTS = transakcja już istnieje
TRANSACTION_UNKNOWN = transakcja nieznana
TX_GROUP_ID_MISMATCH = niezgodność ID grupy transakcji

View File

@@ -23,7 +23,6 @@ import org.qortal.transform.TransformationException;
import org.qortal.transform.block.BlockTransformation;
import org.qortal.utils.BlockArchiveUtils;
import org.qortal.utils.NTP;
import org.qortal.utils.Triple;
import java.io.File;
import java.io.IOException;
@@ -314,9 +313,10 @@ public class BlockArchiveTests extends Common {
repository.getBlockRepository().setBlockPruneHeight(901);
// Prune the AT states for the archived blocks
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(900);
repository.saveChanges();
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 900);
assertEquals(900-1, numATStatesPruned);
assertEquals(900-2, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state
repository.getATRepository().setAtPruneHeight(901);
// Now ensure the SQL repository is missing blocks 2 and 900...
@@ -563,16 +563,23 @@ public class BlockArchiveTests extends Common {
// Trim the first 500 blocks
repository.getBlockRepository().trimOldOnlineAccountsSignatures(0, 500);
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(501);
repository.getATRepository().rebuildLatestAtStates(500);
repository.getATRepository().trimAtStates(0, 500, 1000);
repository.getATRepository().setAtTrimHeight(501);
// Now block 500 should only have the AT state data hash
block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500);
atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500);
// Now block 499 should only have the AT state data hash
List<ATStateData> block499AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(499);
atStatesData = repository.getATRepository().getATStateAtHeight(block499AtStatesData.get(0).getATAddress(), 499);
assertNotNull(atStatesData.getStateHash());
assertNull(atStatesData.getStateData());
// ... but block 501 should have the full data
// ... but block 500 should have the full data (due to being retained as the "latest" AT state in the trimmed range
block500AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(500);
atStatesData = repository.getATRepository().getATStateAtHeight(block500AtStatesData.get(0).getATAddress(), 500);
assertNotNull(atStatesData.getStateHash());
assertNotNull(atStatesData.getStateData());
// ... and block 501 should also have the full data
List<ATStateData> block501AtStatesData = repository.getATRepository().getBlockATStatesAtHeight(501);
atStatesData = repository.getATRepository().getATStateAtHeight(block501AtStatesData.get(0).getATAddress(), 501);
assertNotNull(atStatesData.getStateHash());
@@ -612,9 +619,10 @@ public class BlockArchiveTests extends Common {
repository.getBlockRepository().setBlockPruneHeight(501);
// Prune the AT states for the archived blocks
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(500);
repository.saveChanges();
int numATStatesPruned = repository.getATRepository().pruneAtStates(2, 500);
assertEquals(499, numATStatesPruned);
assertEquals(498, numATStatesPruned); // Minus 1 for genesis block, and another for the latest AT state
repository.getATRepository().setAtPruneHeight(501);
// Now ensure the SQL repository is missing blocks 2 and 500...

View File

@@ -176,7 +176,8 @@ public class BootstrapTests extends Common {
repository.getBlockRepository().setBlockPruneHeight(901);
// Prune the AT states for the archived blocks
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(900);
repository.saveChanges();
repository.getATRepository().pruneAtStates(0, 900);
repository.getATRepository().setAtPruneHeight(901);

View File

@@ -1,16 +1,33 @@
package org.qortal.test;
import com.google.common.hash.HashCode;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.controller.BlockMinter;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.LitecoinACCTv3;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.AtUtils;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import java.util.ArrayList;
import java.util.List;
@@ -19,6 +36,13 @@ import static org.junit.Assert.*;
public class PruneTests extends Common {
// Constants for test AT (an LTC ACCT)
public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long litecoinAmount = 864200L; // 0.00864200 LTC
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@@ -62,23 +86,32 @@ public class PruneTests extends Common {
repository.getBlockRepository().setBlockPruneHeight(6);
// Prune AT states for blocks 2-5
repository.getATRepository().rebuildLatestAtStates(5);
repository.saveChanges();
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 5);
assertEquals(4, numATStatesPruned);
assertEquals(3, numATStatesPruned);
repository.getATRepository().setAtPruneHeight(6);
// Make sure that blocks 2-5 are now missing block data and AT states data
for (Integer i=2; i <= 5; i++) {
// Make sure that blocks 2-4 are now missing block data and AT states data
for (Integer i=2; i <= 4; i++) {
BlockData blockData = repository.getBlockRepository().fromHeight(i);
assertNull(blockData);
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
assertTrue(atStatesDataList.isEmpty());
}
// ... but blocks 6-10 have block data and full AT states data
// Block 5 should have full AT states data even though it was pruned.
// This is because we identified that as the "latest" AT state in that block range
BlockData blockData = repository.getBlockRepository().fromHeight(5);
assertNull(blockData);
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(5);
assertEquals(1, atStatesDataList.size());
// Blocks 6-10 have block data and full AT states data
for (Integer i=6; i <= 10; i++) {
BlockData blockData = repository.getBlockRepository().fromHeight(i);
blockData = repository.getBlockRepository().fromHeight(i);
assertNotNull(blockData.getSignature());
List<ATStateData> atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
atStatesDataList = repository.getATRepository().getBlockATStatesAtHeight(i);
assertNotNull(atStatesDataList);
assertFalse(atStatesDataList.isEmpty());
ATStateData atStatesData = repository.getATRepository().getATStateAtHeight(atStatesDataList.get(0).getATAddress(), i);
@@ -88,4 +121,102 @@ public class PruneTests extends Common {
}
}
@Test
public void testPruneSleepingAt() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = Common.getTestAccount(repository, "alice");
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Mint enough blocks to take the original DEPLOY_AT past the prune threshold (in this case 20)
Block block = BlockUtils.mintBlocks(repository, 25);
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = LitecoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
long txTimestamp = block.getBlockData().getTimestamp();
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress, txTimestamp);
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
// Prune AT states up to block 20
repository.getATRepository().rebuildLatestAtStates(20);
repository.saveChanges();
int numATStatesPruned = repository.getATRepository().pruneAtStates(0, 20);
assertEquals(1, numATStatesPruned); // deleted state at heights 2, but state at height 3 remains
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = LitecoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Test orphaning - should be possible because the previous AT state at height 3 is still available
BlockUtils.orphanLastBlock(repository);
}
}
// Helper methods for AT testing
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = LitecoinACCTv3.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-LTC cross-chain trade";
String description = String.format("Qortal-Litecoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-LTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient, long txTimestamp) throws DataException {
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
}

View File

@@ -50,8 +50,8 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
// Bob self sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Mint blocks
@@ -73,7 +73,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -139,18 +139,18 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Chloe sponsors 10 accounts
List<PrivateKeyAccount> chloeSponsees = generateSponsorshipRewardShares(repository, chloeAccount, 10);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = toRewardShares(repository, chloeAccount, chloeSponsees);
List<PrivateKeyAccount> chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
onlineAccounts.addAll(chloeSponseesOnlineAccounts);
// Dilbert sponsors 5 accounts
List<PrivateKeyAccount> dilbertSponsees = generateSponsorshipRewardShares(repository, dilbertAccount, 5);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = toRewardShares(repository, dilbertAccount, dilbertSponsees);
List<PrivateKeyAccount> dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
onlineAccounts.addAll(dilbertSponseesOnlineAccounts);
// Mint blocks
@@ -172,7 +172,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -270,20 +270,20 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccountsAliceSigner.addAll(bobSponseesOnlineAccounts);
onlineAccountsBobSigner.addAll(bobSponseesOnlineAccounts);
// Chloe sponsors 10 accounts
List<PrivateKeyAccount> chloeSponsees = generateSponsorshipRewardShares(repository, chloeAccount, 10);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = toRewardShares(repository, chloeAccount, chloeSponsees);
List<PrivateKeyAccount> chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
onlineAccountsAliceSigner.addAll(chloeSponseesOnlineAccounts);
onlineAccountsBobSigner.addAll(chloeSponseesOnlineAccounts);
// Dilbert sponsors 5 accounts
List<PrivateKeyAccount> dilbertSponsees = generateSponsorshipRewardShares(repository, dilbertAccount, 5);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = toRewardShares(repository, dilbertAccount, dilbertSponsees);
List<PrivateKeyAccount> dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
onlineAccountsAliceSigner.addAll(dilbertSponseesOnlineAccounts);
onlineAccountsBobSigner.addAll(dilbertSponseesOnlineAccounts);
@@ -306,7 +306,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccountsAliceSigner.addAll(bobSponseeSelfShares);
onlineAccountsBobSigner.addAll(bobSponseeSelfShares);
@@ -382,14 +382,14 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
// Alice sponsors 10 accounts
List<PrivateKeyAccount> aliceSponsees = generateSponsorshipRewardShares(repository, aliceAccount, 10);
List<PrivateKeyAccount> aliceSponseesOnlineAccounts = toRewardShares(repository, aliceAccount, aliceSponsees);
List<PrivateKeyAccount> aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10);
List<PrivateKeyAccount> aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees);
onlineAccountsAliceSigner.addAll(aliceSponseesOnlineAccounts);
onlineAccountsBobSigner.addAll(aliceSponseesOnlineAccounts);
// Bob sponsors 9 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 9);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 9);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccountsAliceSigner.addAll(bobSponseesOnlineAccounts);
onlineAccountsBobSigner.addAll(bobSponseesOnlineAccounts);
@@ -412,7 +412,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> aliceSponseeSelfShares = generateSelfShares(repository, aliceSponsees);
List<PrivateKeyAccount> aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees);
onlineAccountsAliceSigner.addAll(aliceSponseeSelfShares);
onlineAccountsBobSigner.addAll(aliceSponseeSelfShares);
@@ -483,18 +483,18 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Chloe sponsors 10 accounts
List<PrivateKeyAccount> chloeSponsees = generateSponsorshipRewardShares(repository, chloeAccount, 10);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = toRewardShares(repository, chloeAccount, chloeSponsees);
List<PrivateKeyAccount> chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
onlineAccounts.addAll(chloeSponseesOnlineAccounts);
// Dilbert sponsors 5 accounts
List<PrivateKeyAccount> dilbertSponsees = generateSponsorshipRewardShares(repository, dilbertAccount, 5);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = toRewardShares(repository, dilbertAccount, dilbertSponsees);
List<PrivateKeyAccount> dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
onlineAccounts.addAll(dilbertSponseesOnlineAccounts);
// Mint blocks
@@ -516,7 +516,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -597,14 +597,14 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
// Alice sponsors 10 accounts
List<PrivateKeyAccount> aliceSponsees = generateSponsorshipRewardShares(repository, aliceAccount, 10);
List<PrivateKeyAccount> aliceSponseesOnlineAccounts = toRewardShares(repository, aliceAccount, aliceSponsees);
List<PrivateKeyAccount> aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10);
List<PrivateKeyAccount> aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees);
onlineAccounts.addAll(aliceSponseesOnlineAccounts);
onlineAccounts.addAll(aliceSponseesOnlineAccounts);
// Bob sponsors 9 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 9);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 9);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
@@ -627,7 +627,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, aliceSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> aliceSponseeSelfShares = generateSelfShares(repository, aliceSponsees);
List<PrivateKeyAccount> aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees);
onlineAccounts.addAll(aliceSponseeSelfShares);
// Mint blocks (Bob is the signer)
@@ -706,13 +706,13 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount chloeAccount = Common.getTestAccount(repository, "chloe");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Chloe sponsors 10 accounts
List<PrivateKeyAccount> chloeSponsees = generateSponsorshipRewardShares(repository, chloeAccount, 10);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = toRewardShares(repository, chloeAccount, chloeSponsees);
List<PrivateKeyAccount> chloeSponsees = AccountUtils.generateSponsorshipRewardShares(repository, chloeAccount, 10);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
onlineAccounts.addAll(chloeSponseesOnlineAccounts);
// Mint blocks
@@ -728,7 +728,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -744,22 +744,22 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertEquals(19, (int) block.getBlockData().getHeight());
// Bob creates a valid reward share transaction
assertEquals(Transaction.ValidationResult.OK, createRandomRewardShare(repository, bobAccount));
assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount));
// Mint a block, so the algo runs
block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
// Bob can no longer create a reward share transaction
assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, createRandomRewardShare(repository, bobAccount));
assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, AccountUtils.createRandomRewardShare(repository, bobAccount));
// ... but Chloe still can
assertEquals(Transaction.ValidationResult.OK, createRandomRewardShare(repository, chloeAccount));
assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, chloeAccount));
// Orphan last block
BlockUtils.orphanLastBlock(repository);
// Bob creates another valid reward share transaction
assertEquals(Transaction.ValidationResult.OK, createRandomRewardShare(repository, bobAccount));
assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount));
// Run orphan check - this can't be in afterTest() because some tests access the live db
Common.orphanCheck();
@@ -780,13 +780,13 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
// Alice sponsors 10 accounts
List<PrivateKeyAccount> aliceSponsees = generateSponsorshipRewardShares(repository, aliceAccount, 10);
List<PrivateKeyAccount> aliceSponseesOnlineAccounts = toRewardShares(repository, aliceAccount, aliceSponsees);
List<PrivateKeyAccount> aliceSponsees = AccountUtils.generateSponsorshipRewardShares(repository, aliceAccount, 10);
List<PrivateKeyAccount> aliceSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, aliceAccount, aliceSponsees);
onlineAccounts.addAll(aliceSponseesOnlineAccounts);
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Mint blocks
@@ -802,7 +802,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> aliceSponseeSelfShares = generateSelfShares(repository, aliceSponsees);
List<PrivateKeyAccount> aliceSponseeSelfShares = AccountUtils.generateSelfShares(repository, aliceSponsees);
onlineAccounts.addAll(aliceSponseeSelfShares);
// Mint blocks
@@ -818,7 +818,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertEquals(19, (int) block.getBlockData().getHeight());
// Alice creates a valid reward share transaction
assertEquals(Transaction.ValidationResult.OK, createRandomRewardShare(repository, aliceAccount));
assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, aliceAccount));
// Mint a block, so the algo runs
block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
@@ -830,16 +830,16 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertEquals(0, (int) new Account(repository, aliceAccount.getAddress()).getLevel());
// Alice can no longer create a reward share transaction
assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, createRandomRewardShare(repository, aliceAccount));
assertEquals(Transaction.ValidationResult.ACCOUNT_CANNOT_REWARD_SHARE, AccountUtils.createRandomRewardShare(repository, aliceAccount));
// ... but Bob still can
assertEquals(Transaction.ValidationResult.OK, createRandomRewardShare(repository, bobAccount));
assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, bobAccount));
// Orphan last block
BlockUtils.orphanLastBlock(repository);
// Alice creates another valid reward share transaction
assertEquals(Transaction.ValidationResult.OK, createRandomRewardShare(repository, aliceAccount));
assertEquals(Transaction.ValidationResult.OK, AccountUtils.createRandomRewardShare(repository, aliceAccount));
// Run orphan check - this can't be in afterTest() because some tests access the live db
Common.orphanCheck();
@@ -867,8 +867,8 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
// Dilbert sponsors 10 accounts
List<PrivateKeyAccount> dilbertSponsees = generateSponsorshipRewardShares(repository, dilbertAccount, 10);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = toRewardShares(repository, dilbertAccount, dilbertSponsees);
List<PrivateKeyAccount> dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 10);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
onlineAccounts.addAll(dilbertSponseesOnlineAccounts);
// Mint blocks
@@ -921,8 +921,8 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
// Dilbert sponsors 10 accounts
List<PrivateKeyAccount> dilbertSponsees = generateSponsorshipRewardShares(repository, dilbertAccount, 10);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = toRewardShares(repository, dilbertAccount, dilbertSponsees);
List<PrivateKeyAccount> dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 10);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
onlineAccounts.addAll(dilbertSponseesOnlineAccounts);
// Mint blocks
@@ -935,7 +935,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
block = BlockMinter.mintTestingBlock(repository, onlineAccounts.toArray(new PrivateKeyAccount[0]));
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> dilbertSponseeSelfShares = generateSelfShares(repository, dilbertSponsees);
List<PrivateKeyAccount> dilbertSponseeSelfShares = AccountUtils.generateSelfShares(repository, dilbertSponsees);
onlineAccounts.addAll(dilbertSponseeSelfShares);
// Mint blocks
@@ -985,8 +985,8 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount dilbertAccount = Common.getTestAccount(repository, "dilbert");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Chloe sponsors THE SAME 10 accounts
@@ -996,12 +996,12 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
TransactionUtils.signAndImportValid(repository, transactionData, chloeAccount);
}
List<PrivateKeyAccount> chloeSponsees = new ArrayList<>(bobSponsees);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = toRewardShares(repository, chloeAccount, chloeSponsees);
List<PrivateKeyAccount> chloeSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, chloeAccount, chloeSponsees);
onlineAccounts.addAll(chloeSponseesOnlineAccounts);
// Dilbert sponsors 5 accounts
List<PrivateKeyAccount> dilbertSponsees = generateSponsorshipRewardShares(repository, dilbertAccount, 5);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = toRewardShares(repository, dilbertAccount, dilbertSponsees);
List<PrivateKeyAccount> dilbertSponsees = AccountUtils.generateSponsorshipRewardShares(repository, dilbertAccount, 5);
List<PrivateKeyAccount> dilbertSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, dilbertAccount, dilbertSponsees);
onlineAccounts.addAll(dilbertSponseesOnlineAccounts);
// Mint blocks
@@ -1023,7 +1023,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -1108,8 +1108,8 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Mint blocks
@@ -1128,7 +1128,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -1220,8 +1220,8 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Mint blocks
@@ -1240,7 +1240,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -1316,8 +1316,8 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Mint blocks
@@ -1336,7 +1336,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -1392,8 +1392,8 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
// Bob sponsors 10 accounts
List<PrivateKeyAccount> bobSponsees = generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = toRewardShares(repository, bobAccount, bobSponsees);
List<PrivateKeyAccount> bobSponsees = AccountUtils.generateSponsorshipRewardShares(repository, bobAccount, 10);
List<PrivateKeyAccount> bobSponseesOnlineAccounts = AccountUtils.toRewardShares(repository, bobAccount, bobSponsees);
onlineAccounts.addAll(bobSponseesOnlineAccounts);
// Mint blocks
@@ -1412,7 +1412,7 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
assertTrue(new Account(repository, bobSponsee.getAddress()).getLevel() > 0);
// Generate self shares so the sponsees can start minting
List<PrivateKeyAccount> bobSponseeSelfShares = generateSelfShares(repository, bobSponsees);
List<PrivateKeyAccount> bobSponseeSelfShares = AccountUtils.generateSelfShares(repository, bobSponsees);
onlineAccounts.addAll(bobSponseeSelfShares);
// Mint blocks
@@ -1543,61 +1543,6 @@ public class SelfSponsorshipAlgoV1Tests extends Common {
return new TransferPrivsTransaction(repository, transactionData);
}
private static List<PrivateKeyAccount> generateSponsorshipRewardShares(Repository repository, PrivateKeyAccount sponsorAccount, int accountsCount) throws DataException {
final int sharePercent = 0;
Random random = new Random();
List<PrivateKeyAccount> sponsees = new ArrayList<>();
for (int i = 0; i < accountsCount; i++) {
// Generate random sponsee account
byte[] randomPrivateKey = new byte[32];
random.nextBytes(randomPrivateKey);
PrivateKeyAccount sponseeAccount = new PrivateKeyAccount(repository, randomPrivateKey);
sponsees.add(sponseeAccount);
// Create reward-share
TransactionData transactionData = AccountUtils.createRewardShare(repository, sponsorAccount, sponseeAccount, sharePercent, fee);
TransactionUtils.signAndImportValid(repository, transactionData, sponsorAccount);
}
return sponsees;
}
private static Transaction.ValidationResult createRandomRewardShare(Repository repository, PrivateKeyAccount account) throws DataException {
// Bob attempts to create a reward share transaction
byte[] randomPrivateKey = new byte[32];
new Random().nextBytes(randomPrivateKey);
PrivateKeyAccount sponseeAccount = new PrivateKeyAccount(repository, randomPrivateKey);
TransactionData transactionData = AccountUtils.createRewardShare(repository, account, sponseeAccount, 0, fee);
return TransactionUtils.signAndImport(repository, transactionData, account);
}
private static List<PrivateKeyAccount> generateSelfShares(Repository repository, List<PrivateKeyAccount> accounts) throws DataException {
final int sharePercent = 0;
for (PrivateKeyAccount account : accounts) {
// Create reward-share
TransactionData transactionData = AccountUtils.createRewardShare(repository, account, account, sharePercent, 0L);
TransactionUtils.signAndImportValid(repository, transactionData, account);
}
return toRewardShares(repository, null, accounts);
}
private static List<PrivateKeyAccount> toRewardShares(Repository repository, PrivateKeyAccount parentAccount, List<PrivateKeyAccount> accounts) {
List<PrivateKeyAccount> rewardShares = new ArrayList<>();
for (PrivateKeyAccount account : accounts) {
PrivateKeyAccount sponsor = (parentAccount != null) ? parentAccount : account;
byte[] rewardSharePrivateKey = sponsor.getRewardSharePrivateKey(account.getPublicKey());
PrivateKeyAccount rewardShareAccount = new PrivateKeyAccount(repository, rewardSharePrivateKey);
rewardShares.add(rewardShareAccount);
}
return rewardShares;
}
private boolean areAllAccountsPresentInBlock(List<PrivateKeyAccount> accounts, Block block) throws DataException {
for (PrivateKeyAccount bobSponsee : accounts) {
boolean foundOnlineAccountInBlock = false;

View File

@@ -84,7 +84,7 @@ public class BlockApiTests extends ApiCommon {
@Test
public void testGetBlockRange() {
assertNotNull(this.blocksResource.getBlockRange(1, 1));
assertNotNull(this.blocksResource.getBlockRange(1, 1, false, false));
List<Integer> testValues = Arrays.asList(null, Integer.valueOf(1));

View File

@@ -1,11 +1,26 @@
package org.qortal.test.arbitrary;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataReader;
import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Service;
import org.qortal.arbitrary.misc.Service.ValidationResult;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.ArbitraryUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.utils.Base58;
import java.io.IOException;
import java.nio.file.Files;
@@ -117,10 +132,27 @@ public class ArbitraryServiceTests extends Common {
Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
// There is an index file in the root
assertEquals(ValidationResult.OK, service.validate(path));
}
@Test
public void testValidateSingleFileGifRepository() throws IOException {
// Generate some random data
byte[] data = new byte[1024];
new Random().nextBytes(data);
// Write the data to a single file in a temp path
Path path = Files.createTempDirectory("testValidateSingleFileGifRepository");
path.toFile().deleteOnExit();
Path imagePath = Paths.get(path.toString(), "image1.gif");
Files.write(imagePath, data, StandardOpenOption.CREATE);
Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.OK, service.validate(imagePath));
}
@Test
public void testValidateMultiLayerGifRepository() throws IOException {
// Generate some random data
@@ -140,7 +172,6 @@ public class ArbitraryServiceTests extends Common {
Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
// There is an index file in the root
assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
}
@@ -151,7 +182,6 @@ public class ArbitraryServiceTests extends Common {
Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
// There is an index file in the root
assertEquals(ValidationResult.MISSING_DATA, service.validate(path));
}
@@ -171,8 +201,192 @@ public class ArbitraryServiceTests extends Common {
Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
// There is an index file in the root
assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
}
}
@Test
public void testValidatePublishedGifRepository() throws IOException, DataException, MissingDataException, IllegalAccessException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Generate some random data
byte[] data = new byte[1024];
new Random().nextBytes(data);
// Write the data to several files in a temp path
Path path = Files.createTempDirectory("testValidateGifRepository");
path.toFile().deleteOnExit();
Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
Files.write(Paths.get(path.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
Files.write(Paths.get(path.toString(), "image3.gif"), data, StandardOpenOption.CREATE);
Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.OK, service.validate(path));
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String publicKey58 = Base58.encode(alice.getPublicKey());
String name = "TEST"; // Can be anything for this test
String identifier = "test_identifier";
// Register the name to Alice
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData, alice);
// Set difficulty to 1
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
// Create PUT transaction
ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice);
// Build the latest data state for this name, and no exceptions should be thrown because validation passes
ArbitraryDataReader arbitraryDataReader1a = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
arbitraryDataReader1a.loadSynchronously(true);
}
}
@Test
public void testValidateQChatAttachment() throws IOException {
// Generate some random data
byte[] data = new byte[1024];
new Random().nextBytes(data);
// Write the data a single file in a temp path
Path path = Files.createTempDirectory("testValidateQChatAttachment");
path.toFile().deleteOnExit();
Files.write(Paths.get(path.toString(), "document.pdf"), data, StandardOpenOption.CREATE);
Service service = Service.QCHAT_ATTACHMENT;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.OK, service.validate(path));
}
@Test
public void testValidateSingleFileQChatAttachment() throws IOException {
// Generate some random data
byte[] data = new byte[1024];
new Random().nextBytes(data);
// Write the data a single file in a temp path
Path path = Files.createTempDirectory("testValidateSingleFileQChatAttachment");
path.toFile().deleteOnExit();
Path filePath = Paths.get(path.toString(), "document.pdf");
Files.write(filePath, data, StandardOpenOption.CREATE);
Service service = Service.QCHAT_ATTACHMENT;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.OK, service.validate(filePath));
}
@Test
public void testValidateInvalidQChatAttachmentFileExtension() throws IOException {
// Generate some random data
byte[] data = new byte[1024];
new Random().nextBytes(data);
// Write the data a single file in a temp path
Path path = Files.createTempDirectory("testValidateInvalidQChatAttachmentFileExtension");
path.toFile().deleteOnExit();
Files.write(Paths.get(path.toString(), "application.exe"), data, StandardOpenOption.CREATE);
Service service = Service.QCHAT_ATTACHMENT;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
}
@Test
public void testValidateEmptyQChatAttachment() throws IOException {
Path path = Files.createTempDirectory("testValidateEmptyQChatAttachment");
Service service = Service.QCHAT_ATTACHMENT;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.INVALID_FILE_COUNT, service.validate(path));
}
@Test
public void testValidateMultiLayerQChatAttachment() throws IOException {
// Generate some random data
byte[] data = new byte[1024];
new Random().nextBytes(data);
// Write the data to several files in a temp path
Path path = Files.createTempDirectory("testValidateMultiLayerQChatAttachment");
path.toFile().deleteOnExit();
Files.write(Paths.get(path.toString(), "file1.txt"), data, StandardOpenOption.CREATE);
Path subdirectory = Paths.get(path.toString(), "subdirectory");
Files.createDirectories(subdirectory);
Files.write(Paths.get(subdirectory.toString(), "file2.txt"), data, StandardOpenOption.CREATE);
Files.write(Paths.get(subdirectory.toString(), "file3.txt"), data, StandardOpenOption.CREATE);
Service service = Service.QCHAT_ATTACHMENT;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
}
@Test
public void testValidateMultiFileQChatAttachment() throws IOException {
// Generate some random data
byte[] data = new byte[1024];
new Random().nextBytes(data);
// Write the data to several files in a temp path
Path path = Files.createTempDirectory("testValidateMultiFileQChatAttachment");
path.toFile().deleteOnExit();
Files.write(Paths.get(path.toString(), "file1.txt"), data, StandardOpenOption.CREATE);
Files.write(Paths.get(path.toString(), "file2.txt"), data, StandardOpenOption.CREATE);
Service service = Service.QCHAT_ATTACHMENT;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.INVALID_FILE_COUNT, service.validate(path));
}
@Test
public void testValidatePublishedQChatAttachment() throws IOException, DataException, MissingDataException, IllegalAccessException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Generate some random data
byte[] data = new byte[1024];
new Random().nextBytes(data);
// Write the data a single file in a temp path
Path path = Files.createTempDirectory("testValidateSingleFileQChatAttachment");
path.toFile().deleteOnExit();
Path filePath = Paths.get(path.toString(), "document.pdf");
Files.write(filePath, data, StandardOpenOption.CREATE);
Service service = Service.QCHAT_ATTACHMENT;
assertTrue(service.isValidationRequired());
assertEquals(ValidationResult.OK, service.validate(filePath));
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String publicKey58 = Base58.encode(alice.getPublicKey());
String name = "TEST"; // Can be anything for this test
String identifier = "test_identifier";
// Register the name to Alice
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData, alice);
// Set difficulty to 1
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
// Create PUT transaction
ArbitraryUtils.createAndMintTxn(repository, publicKey58, filePath, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice);
// Build the latest data state for this name, and no exceptions should be thrown because validation passes
ArbitraryDataReader arbitraryDataReader1a = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
arbitraryDataReader1a.loadSynchronously(true);
}
}
}

View File

@@ -12,6 +12,7 @@ import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.arbitrary.misc.Category;
import org.qortal.arbitrary.misc.Service;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.RegisterNameTransactionData;
import org.qortal.repository.DataException;
@@ -25,9 +26,13 @@ import org.qortal.transaction.RegisterNameTransaction;
import org.qortal.utils.Base58;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.*;
@@ -279,6 +284,94 @@ public class ArbitraryTransactionMetadataTests extends Common {
}
}
@Test
public void testSingleFileList() throws DataException, IOException, MissingDataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String publicKey58 = Base58.encode(alice.getPublicKey());
String name = "TEST"; // Can be anything for this test
String identifier = null; // Not used for this test
Service service = Service.ARBITRARY_DATA;
int chunkSize = 100;
int dataLength = 900; // Actual data length will be longer due to encryption
// Register the name to Alice
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData, alice);
// Add a few files at multiple levels
byte[] data = new byte[1024];
new Random().nextBytes(data);
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
Path file1 = Paths.get(path1.toString(), "file.txt");
// Create PUT transaction
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, file1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
// Check the file list metadata is correct
assertEquals(1, arbitraryDataFile.getMetadata().getFiles().size());
assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("file.txt"));
// Ensure the file list can be read back out again, when specified to be included
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(arbitraryDataFile.getMetadata(), true);
assertTrue(resourceMetadata.getFiles().contains("file.txt"));
// Ensure it's not returned when specified to be excluded
// The entire object will be null because there is no metadata
ArbitraryResourceMetadata resourceMetadataSimple = ArbitraryResourceMetadata.fromTransactionMetadata(arbitraryDataFile.getMetadata(), false);
assertNull(resourceMetadataSimple);
}
}
@Test
public void testMultipleFileList() throws DataException, IOException, MissingDataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
String publicKey58 = Base58.encode(alice.getPublicKey());
String name = "TEST"; // Can be anything for this test
String identifier = null; // Not used for this test
Service service = Service.ARBITRARY_DATA;
int chunkSize = 100;
int dataLength = 900; // Actual data length will be longer due to encryption
// Register the name to Alice
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
TransactionUtils.signAndMint(repository, transactionData, alice);
// Add a few files at multiple levels
byte[] data = new byte[1024];
new Random().nextBytes(data);
Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength);
Files.write(Paths.get(path1.toString(), "image1.jpg"), data, StandardOpenOption.CREATE);
Path subdirectory = Paths.get(path1.toString(), "subdirectory");
Files.createDirectories(subdirectory);
Files.write(Paths.get(subdirectory.toString(), "config.json"), data, StandardOpenOption.CREATE);
// Create PUT transaction
ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
// Check the file list metadata is correct
assertEquals(3, arbitraryDataFile.getMetadata().getFiles().size());
assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("file.txt"));
assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("image1.jpg"));
assertTrue(arbitraryDataFile.getMetadata().getFiles().contains("subdirectory/config.json"));
// Ensure the file list can be read back out again, when specified to be included
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(arbitraryDataFile.getMetadata(), true);
assertTrue(resourceMetadata.getFiles().contains("file.txt"));
assertTrue(resourceMetadata.getFiles().contains("image1.jpg"));
assertTrue(resourceMetadata.getFiles().contains("subdirectory/config.json"));
// Ensure it's not returned when specified to be excluded
// The entire object will be null because there is no metadata
ArbitraryResourceMetadata resourceMetadataSimple = ArbitraryResourceMetadata.fromTransactionMetadata(arbitraryDataFile.getMetadata(), false);
assertNull(resourceMetadataSimple);
}
}
@Test
public void testExistingCategories() {
// Matching categories should be correctly located

View File

@@ -2,29 +2,20 @@ package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.List;
import org.ciyam.at.CompilationException;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.AtUtils;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
public class AtRepositoryTests extends Common {
@@ -76,7 +67,7 @@ public class AtRepositoryTests extends Common {
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxHeight);
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
@@ -130,7 +121,7 @@ public class AtRepositoryTests extends Common {
Integer testHeight = blockchainHeight;
// Trim AT state data
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxHeight);
// COMMIT to check latest AT states persist / TEMPORARY table interaction
repository.saveChanges();
@@ -163,8 +154,8 @@ public class AtRepositoryTests extends Common {
int maxTrimHeight = blockchainHeight - 4;
Integer testHeight = maxTrimHeight + 1;
// Trim AT state data
repository.getATRepository().rebuildLatestAtStates();
// Trim AT state data (using a max height of maxTrimHeight + 1, so it is beyond the trimmed range)
repository.getATRepository().rebuildLatestAtStates(maxTrimHeight + 1);
repository.saveChanges();
repository.getATRepository().trimAtStates(2, maxTrimHeight, 1000);
@@ -333,7 +324,7 @@ public class AtRepositoryTests extends Common {
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().rebuildLatestAtStates();
repository.getATRepository().rebuildLatestAtStates(maxHeight);
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);

View File

@@ -8,7 +8,6 @@ import java.util.*;
import com.google.common.primitives.Longs;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.Qortal25519Extras;
import org.qortal.data.network.OnlineAccountData;
@@ -19,6 +18,7 @@ import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction;
import org.qortal.transform.Transformer;
import org.qortal.utils.Amounts;
@@ -86,6 +86,61 @@ public class AccountUtils {
return rewardSharePrivateKey;
}
public static List<PrivateKeyAccount> generateSponsorshipRewardShares(Repository repository, PrivateKeyAccount sponsorAccount, int accountsCount) throws DataException {
final int sharePercent = 0;
Random random = new Random();
List<PrivateKeyAccount> sponsees = new ArrayList<>();
for (int i = 0; i < accountsCount; i++) {
// Generate random sponsee account
byte[] randomPrivateKey = new byte[32];
random.nextBytes(randomPrivateKey);
PrivateKeyAccount sponseeAccount = new PrivateKeyAccount(repository, randomPrivateKey);
sponsees.add(sponseeAccount);
// Create reward-share
TransactionData transactionData = AccountUtils.createRewardShare(repository, sponsorAccount, sponseeAccount, sharePercent, fee);
TransactionUtils.signAndImportValid(repository, transactionData, sponsorAccount);
}
return sponsees;
}
public static Transaction.ValidationResult createRandomRewardShare(Repository repository, PrivateKeyAccount account) throws DataException {
// Bob attempts to create a reward share transaction
byte[] randomPrivateKey = new byte[32];
new Random().nextBytes(randomPrivateKey);
PrivateKeyAccount sponseeAccount = new PrivateKeyAccount(repository, randomPrivateKey);
TransactionData transactionData = createRewardShare(repository, account, sponseeAccount, 0, fee);
return TransactionUtils.signAndImport(repository, transactionData, account);
}
public static List<PrivateKeyAccount> generateSelfShares(Repository repository, List<PrivateKeyAccount> accounts) throws DataException {
final int sharePercent = 0;
for (PrivateKeyAccount account : accounts) {
// Create reward-share
TransactionData transactionData = createRewardShare(repository, account, account, sharePercent, 0L);
TransactionUtils.signAndImportValid(repository, transactionData, account);
}
return toRewardShares(repository, null, accounts);
}
public static List<PrivateKeyAccount> toRewardShares(Repository repository, PrivateKeyAccount parentAccount, List<PrivateKeyAccount> accounts) {
List<PrivateKeyAccount> rewardShares = new ArrayList<>();
for (PrivateKeyAccount account : accounts) {
PrivateKeyAccount sponsor = (parentAccount != null) ? parentAccount : account;
byte[] rewardSharePrivateKey = sponsor.getRewardSharePrivateKey(account.getPublicKey());
PrivateKeyAccount rewardShareAccount = new PrivateKeyAccount(repository, rewardSharePrivateKey);
rewardShares.add(rewardShareAccount);
}
return rewardShares;
}
public static Map<String, Map<Long, Long>> getBalances(Repository repository, long... assetIds) throws DataException {
Map<String, Map<Long, Long>> balances = new HashMap<>();

View File

@@ -20,6 +20,15 @@ public class BlockUtils {
return BlockMinter.mintTestingBlock(repository, mintingAccount);
}
/** Mints multiple blocks using "alice-reward-share" test account, and returns the final block. */
public static Block mintBlocks(Repository repository, int count) throws DataException {
Block block = null;
for (int i=0; i<count; i++) {
block = BlockUtils.mintBlock(repository);
}
return block;
}
public static Long getNextBlockReward(Repository repository) throws DataException {
int currentHeight = repository.getBlockRepository().getBlockchainHeight();

View File

@@ -120,7 +120,9 @@ public class Common {
}
public static void useSettingsAndDb(String settingsFilename, boolean dbInMemory) throws DataException {
closeRepository();
if (RepositoryManager.getRepositoryFactory() != null) {
closeRepository();
}
// Load/check settings, which potentially sets up blockchain config, etc.
LOGGER.debug(String.format("Using setting file: %s", settingsFilename));

View File

@@ -0,0 +1,40 @@
package org.qortal.test.common.transaction;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import java.util.Random;
public class ChatTestTransaction extends TestTransaction {
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
Random random = new Random();
byte[] orderId = new byte[64];
random.nextBytes(orderId);
String sender = Crypto.toAddress(account.getPublicKey());
int nonce = 1234567;
// Generate random recipient
byte[] randomPrivateKey = new byte[32];
random.nextBytes(randomPrivateKey);
PrivateKeyAccount recipientAccount = new PrivateKeyAccount(repository, randomPrivateKey);
String recipient = Crypto.toAddress(recipientAccount.getPublicKey());
byte[] chatReference = new byte[64];
random.nextBytes(chatReference);
byte[] data = new byte[4000];
random.nextBytes(data);
boolean isText = true;
boolean isEncrypted = true;
return new ChatTransactionData(generateBase(account), sender, nonce, recipient, chatReference, data, isText, isEncrypted);
}
}

View File

@@ -135,7 +135,8 @@ public class AdminTests extends Common {
assertNotSame(ValidationResult.OK, result);
// Attempt to ban Bob
result = groupBan(repository, alice, groupId, bob.getAddress());
int timeToLive = 0;
result = groupBan(repository, alice, groupId, bob.getAddress(), timeToLive);
// Should be OK
assertEquals(ValidationResult.OK, result);
@@ -158,7 +159,7 @@ public class AdminTests extends Common {
assertTrue(isMember(repository, bob.getAddress(), groupId));
// Attempt to ban Bob
result = groupBan(repository, alice, groupId, bob.getAddress());
result = groupBan(repository, alice, groupId, bob.getAddress(), timeToLive);
// Should be OK
assertEquals(ValidationResult.OK, result);
@@ -205,6 +206,144 @@ public class AdminTests extends Common {
}
}
@Test
public void testGroupBanMemberWithExpiry() throws DataException, InterruptedException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Create group
int groupId = createGroup(repository, alice, "open-group", true);
// Confirm Bob is not a member
assertFalse(isMember(repository, bob.getAddress(), groupId));
// Attempt to cancel non-existent Bob ban
ValidationResult result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
// Should NOT be OK
assertNotSame(ValidationResult.OK, result);
// Attempt to ban Bob for 2 seconds
int timeToLive = 2;
result = groupBan(repository, alice, groupId, bob.getAddress(), timeToLive);
// Should be OK
assertEquals(ValidationResult.OK, result);
// Confirm Bob no longer a member
assertFalse(isMember(repository, bob.getAddress(), groupId));
// Bob attempts to rejoin
result = joinGroup(repository, bob, groupId);
// Should NOT be OK
assertNotSame(ValidationResult.OK, result);
// Wait for 2 seconds to pass
Thread.sleep(2000L);
// Bob attempts to rejoin again
result = joinGroup(repository, bob, groupId);
// Should be OK, as the ban has expired
assertSame(ValidationResult.OK, result);
// Confirm Bob is now a member
assertTrue(isMember(repository, bob.getAddress(), groupId));
// Orphan last block (Bob join)
BlockUtils.orphanLastBlock(repository);
// Confirm Bob is not a member
assertFalse(isMember(repository, bob.getAddress(), groupId));
// Orphan last block (Bob ban)
BlockUtils.orphanLastBlock(repository);
// Delete unconfirmed group-ban transaction
TransactionUtils.deleteUnconfirmedTransactions(repository);
// Bob to join
result = joinGroup(repository, bob, groupId);
// Should be OK
assertEquals(ValidationResult.OK, result);
// Confirm Bob now a member
assertTrue(isMember(repository, bob.getAddress(), groupId));
// Attempt to ban Bob for 2 seconds
result = groupBan(repository, alice, groupId, bob.getAddress(), 2);
// Should be OK
assertEquals(ValidationResult.OK, result);
// Confirm Bob no longer a member
assertFalse(isMember(repository, bob.getAddress(), groupId));
// Wait for 2 seconds to pass
Thread.sleep(2000L);
// Cancel Bob's ban
result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
// Should NOT be OK, as ban has already expired
assertNotSame(ValidationResult.OK, result);
// Confirm Bob still not a member
assertFalse(isMember(repository, bob.getAddress(), groupId));
// Bob attempts to rejoin
result = joinGroup(repository, bob, groupId);
// Should be OK, as no longer banned
assertSame(ValidationResult.OK, result);
// Confirm Bob is now a member
assertTrue(isMember(repository, bob.getAddress(), groupId));
// Attempt to ban Bob for 10 seconds
result = groupBan(repository, alice, groupId, bob.getAddress(), 10);
// Should be OK
assertEquals(ValidationResult.OK, result);
// Confirm Bob no longer a member
assertFalse(isMember(repository, bob.getAddress(), groupId));
// Bob attempts to rejoin
result = joinGroup(repository, bob, groupId);
// Should NOT be OK, as ban still exists
assertNotSame(ValidationResult.OK, result);
// Cancel Bob's ban
result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
// Should be OK, as ban still exists
assertEquals(ValidationResult.OK, result);
// Bob attempts to rejoin
result = joinGroup(repository, bob, groupId);
// Should be OK, as no longer banned
assertEquals(ValidationResult.OK, result);
// Orphan last block (Bob join)
BlockUtils.orphanLastBlock(repository);
// Delete unconfirmed join-group transaction
TransactionUtils.deleteUnconfirmedTransactions(repository);
// Orphan last block (Cancel Bob ban)
BlockUtils.orphanLastBlock(repository);
// Delete unconfirmed cancel-ban transaction
TransactionUtils.deleteUnconfirmedTransactions(repository);
// Bob attempts to rejoin
result = joinGroup(repository, bob, groupId);
// Should NOT be OK
assertNotSame(ValidationResult.OK, result);
// Orphan last block (Bob ban)
BlockUtils.orphanLastBlock(repository);
// Delete unconfirmed group-ban transaction
TransactionUtils.deleteUnconfirmedTransactions(repository);
// Confirm Bob now a member
assertTrue(isMember(repository, bob.getAddress(), groupId));
}
}
@Test
public void testGroupBanAdmin() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -226,7 +365,8 @@ public class AdminTests extends Common {
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
// Attempt to ban Bob
result = groupBan(repository, alice, groupId, bob.getAddress());
int timeToLive = 0;
result = groupBan(repository, alice, groupId, bob.getAddress(), timeToLive);
// Should be OK
assertEquals(ValidationResult.OK, result);
@@ -272,12 +412,12 @@ public class AdminTests extends Common {
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
// Have Alice (owner) try to ban herself!
result = groupBan(repository, alice, groupId, alice.getAddress());
result = groupBan(repository, alice, groupId, alice.getAddress(), timeToLive);
// Should NOT be OK
assertNotSame(ValidationResult.OK, result);
// Have Bob try to ban Alice (owner)
result = groupBan(repository, bob, groupId, alice.getAddress());
result = groupBan(repository, bob, groupId, alice.getAddress(), timeToLive);
// Should NOT be OK
assertNotSame(ValidationResult.OK, result);
}
@@ -316,8 +456,8 @@ public class AdminTests extends Common {
return result;
}
private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0);
private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member, int timeToLive) throws DataException {
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", timeToLive);
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
if (result == ValidationResult.OK)

Some files were not shown because too many files have changed in this diff Show More