Compare commits

...

87 Commits

Author SHA1 Message Date
catbref
69ec654e4a Bump to version 1.3.7 2020-11-11 09:27:25 +00:00
catbref
a310e751bb Fix slow SQL query in HSQLDBATRepository.getBlockATStatesAtHeight() - mostly used during orphaning 2020-11-10 16:58:24 +00:00
catbref
3ef8b81e51 Fix incorrect column indexes when fetching frozen AT data 2020-11-10 15:38:15 +00:00
catbref
1f409235e4 Don't rebuild repository or export node-local data during repository build if repository was 'pristine'.
Under certain conditions, e.g. non-existent database files, the repository would be created
and then immediately be re-created.

Not only was this unnecessary, but HSQLDBDatabaseUpdates would attempt to export the node-local
data twice, which would cause an error due to existing .script files.

The fix is three-pronged:

1. Don't immediately rebuild the repository if it's only just been built
2. Don't export the empty node-local data if repository has only just been built
3. Don't export the node-local data if it's empty
2020-11-09 10:31:21 +00:00
catbref
806baa6ae4 Fix API call referenced in DB reshape 2020-11-06 11:23:35 +00:00
catbref
58ed72058f Another attempt to prevent "serialization failure" during trimming 2020-11-05 14:36:14 +00:00
catbref
253a994438 Add API POST /repository/checkpoint call. Renamed GET/POST /admin/repository calls to /admin/repository/data 2020-11-05 11:08:54 +00:00
catbref
5549eded38 Improve/fix use of latest block cache, for more cache hits, faster chain-tip response, etc. 2020-11-05 09:34:57 +00:00
catbref
20777363cf Split AT state storage into two HSQLDB table for better management
This involves a database reshape, but before this happens the node-local
data is exported to local files, giving the user the option to use a
bootstrap file instead of waiting.
2020-11-04 20:07:30 +00:00
catbref
b3f859f290 Don't use WITH COLUMN NAMES when exporting data from repository into local file 2020-11-04 15:48:52 +00:00
catbref
8c9f68a9c3 Add API calls for exporting node-local repository data & corresponding import to/from local files 2020-11-04 15:35:42 +00:00
catbref
41f178bf59 Add support for API key security, where X-API-KEY header must match apiKey from settings
apiKey in settings is null by default at this point, for backwards compatibility.
In the future, the Windows installer could generate a UUID for apiKey.
apiKey in settings needs to be at least 8 characters.

API calls in the documentation engine are now marked with an open/closed padlock
to show where API key might be required.
Add support for API key security, where X-API-KEY header must match apiKey from settings

apiKey in settings is null by default at this point, for backwards compatibility.
In the future, the Windows installer could generate a UUID for apiKey.
apiKey in settings needs to be at least 8 characters.

API calls in the documentation engine are now marked with an open/closed padlock
to show where API key might be required.
2020-11-04 15:35:20 +00:00
catbref
ad5050f92e Add support for exporting node-local repository data to .script files and corresponding import function 2020-11-04 15:29:10 +00:00
catbref
16397852ae Add synchronization around updating trim heights to prevent deadlock/rollback 2020-11-04 10:01:20 +00:00
catbref
c125a53655 More (optionally) logging when comparing chains with peers. Support for potential future minor consensus change. 2020-11-02 11:52:06 +00:00
catbref
7b056a832f Turn off HSQLDB redo-log "blockchain.log" and periodically call "CHECKPOINT" instead.
Checkpointing interval is 1 hour by default, changable in settings via
"repositoryCheckpointInterval"
plus corresponding "showCheckpointNotifications" SysTray flags (off by default).

Added entries to SysTray_en i18n properties, and converted SysTray_ru to ISO-8559-1.
2020-11-02 11:49:21 +00:00
catbref
6c40727027 More reporting for slow HSQLDB queries/commits 2020-11-02 11:16:40 +00:00
catbref
8f06765caf Networking performance improvements and message sending bugfix 2020-11-02 11:15:53 +00:00
catbref
de2fc78ad1 Add support for SHA256 digest of ByteBuffer in Crypto class 2020-11-02 10:47:27 +00:00
catbref
ee08410260 More trace-level debugging in Synchronizer to help diagnose chain reorg issues 2020-11-02 10:46:51 +00:00
catbref
88da8d949f Don't allow latest blocks cache to be empty 2020-11-02 10:45:21 +00:00
catbref
d2a92db921 More caching for GetBlockMessage. Added API call GET /admin/enginestats to monitor cache usage 2020-10-29 11:02:02 +00:00
catbref
9c18a33d7f Improve tools/build-auto-update.sh when working on detached HEAD 2020-10-28 09:08:16 +00:00
catbref
f3b8258067 Add more latest block caching to reduce repository accesses, especially for requests from remote peers 2020-10-28 08:46:30 +00:00
catbref
da78c73485 Remove extraneous call to Controller.onNewBlock() after synchronization, as this call is performed per-block inside Synchronizer 2020-10-28 08:42:59 +00:00
catbref
cec25ce279 Add API call POST /peers/commonblock <connected-peer> as debugging aid 2020-10-28 08:41:23 +00:00
catbref
0389007491 Skip trimming while performing synchronization 2020-10-28 08:38:11 +00:00
catbref
38a64bdd9e Prevent HSQLDB prepared statement cache invalidation when rebuilding latest AT states cache 2020-10-12 14:35:10 +01:00
catbref
6a24f787c4 Bump to v1.3.6 2020-10-07 14:56:58 +01:00
catbref
98564aa8bf Fix SQL logic error when fetching trade offers 2020-10-07 14:56:05 +01:00
catbref
9ceff90f42 Upgrade to CIYAM-AT v1.3.8 with slight performance improvements 2020-10-07 10:31:18 +01:00
catbref
6a4388fecc Use cached PreparedStatement for HSQLDB.assertEmptyTransaction + other minor HSQLDB fixes 2020-10-07 09:45:44 +01:00
catbref
1958444bc4 Add recipient indexes for payment/AT transactions to speed up AT processing 2020-10-06 14:09:42 +01:00
catbref
a2038274e1 Keep latest AT state, even if "finished", so we can produce historical trade data 2020-10-05 15:18:41 +01:00
catbref
532c697026 Moved AT State & online signatures trimming intervals, batch sizes, limits, etc. to Settings 2020-10-02 12:58:23 +01:00
catbref
5cf5c1e1f7 Take pressure off GC by not creating/destroying HSQLDB sub-repositories all the time 2020-10-01 13:18:24 +01:00
catbref
60621e8b81 Reworked AT-states and online signatures trimming
Instead of searching from block 0, we now keep a record of
base trim height in the DB itself.

Also, we no longer trim the latest AT state for non-finished ATs
in case they are in deep sleeping and we need their state for when
they awaken.
2020-10-01 13:17:00 +01:00
catbref
a6a1f65d3e Reduce block search size in AT state trimmer to reduce load 2020-09-29 11:41:30 +01:00
catbref
a681f741dd Add initial delay before trimming online accounts signatures 2020-09-29 11:40:41 +01:00
catbref
bed9837967 Added settings entry "localeLang" for controlling core language (not-API) 2020-09-29 10:56:27 +01:00
catbref
855cb2226a Aggressively trim old AT state data and online accounts signatures.
Two new classes/threads made to quickly find first trimmable row
then repeatedly trim rows in small batches after that.
2020-09-28 14:34:00 +01:00
catbref
d85a3d17c8 Fix for HSQLDB deadlock during CHECKPOINT.
Symptoms are:

* db/blockchain.log is pretty much exactly 50MB - the checkpoint-triggering size.

* Loads of threads are stuck waiting for HSQLDB's CountUpDownLatch$Sync.await()

* Synchronizer, or some other thread, possibly orphaning blocks.

The cause seems to be method A, which has a repository session,
calls EventBus.INSTANCE.notify() and one of the event listeners
then obtains their own repository session to do repository 'work'.

In the meantime, the HSQLDB log has reached 50MB, triggering auto-checkpoint.

HSQLDB attempts to CHECKPOINT, but waits for existing transactions
to complete, and also blocks starting new transactions.

Thus, one of the event listeners is blocked when they try to obtain
a new repository session, but HSQLDB never performs CHECKPOINT
because the event notifier (method A) still has an unfinished
transaction - hence deadlock.
2020-09-28 14:22:18 +01:00
catbref
81a5b154c2 Add API call DELETE /admin/repository which actually performs repository maintenance (takes several minutes) 2020-09-25 17:06:06 +01:00
catbref
a6f42df9d6 Add isTestNet to API call GET /admin/info 2020-09-25 16:35:53 +01:00
catbref
17ae7acc6d Reduce DB storage of AT states
Drop created_when column from ATStates as it never changes
and can be fetched from ATs table.
This takes about 50s on a fast machine.

Correspondingly rebuild height-based index on ATStates.
This takes about 3 minutes on a fast machine.

Modify AT-related repository methods and callers.

Aggressively remove 'old' (> 2 weeks) actual AT
state binary data, leaving only the hash in DB
(for syncing purposes). Seems to keep up with
syncing from another node on localhost.
2020-09-25 15:25:57 +01:00
catbref
3d5fec3c30 Bump to HSQLDB v2.5.1 as we seem clear of OOM issue 2020-09-25 15:25:15 +01:00
catbref
21f48fba5f HSQLDB PreparedStatement caching improvements 2020-09-24 12:46:30 +01:00
catbref
d0da5d7c48 ATs: only call MachineState.getCodeBytes() once in preparation for using newer AT lib 2020-09-24 12:46:17 +01:00
catbref
4209cc6ee4 Improve SQL prepared statement caching in HSQLDBATRepository, plus missing space in SQL in getMatchingFinalATStates 2020-09-23 09:41:24 +01:00
catbref
f3e1092dd5 Improve SQL prepared statement caching in HSQLDBBlockRepository.getBlockInfos & test to cover 2020-09-23 09:40:41 +01:00
catbref
43055b666f Improve SQL prepared statement caching in HSQLDBAccountRepository.getEligibleLegacyQoraHolders() 2020-09-23 09:38:08 +01:00
catbref
1720582f33 Remove obsolete NextBlockHeight table and corresponding triggers
Also fix typo in Block.online_accounts varbinary size.
2020-09-11 16:06:53 +01:00
catbref
d93e9d570f Trimming old block online accounts signatures
There's still an existing issue where log entries like this appear:

  Unable to trim old online accounts signatures in repository

which is actually caused by:

  integrity constraint violation: unique constraint or index violation; SYS_PK_10092 table: BLOCKS

which seems to be a bug in the version of HSQLDB we use.
(Tested using synced-from-scratch DB).
It's not clear what the actual problem is at this point.

It might be possible to switch to v2.5.1 if our recent HSQLDB-related
commits have fixed/worked-around the OOM issues.

Move the inner method from BlockChain to Controller.
Remove blockchain lock as it's not needed because it's not an
HSQLDB "serialization failure" but constraint violation.

Trimming old online accounts signatures limited to batches of 1440
rows to reduce CPU and memory load.
2020-09-11 15:57:12 +01:00
catbref
5ea90f2fdd Speed up fetching transactions using block signature 2020-09-11 15:56:19 +01:00
catbref
c628f97d8c Speed up fetching block height based on timestamp 2020-09-11 15:55:54 +01:00
catbref
8a1e2f4111 Reduce HSQLDBRepository log noise by omitting idle session info 2020-09-11 15:54:32 +01:00
catbref
41f244d549 Add bitcoinj Context propagation 2020-09-11 15:52:59 +01:00
catbref
79641efa87 Tighten up trade-bot, ElectrumX
Added separate method to determine status of P2SH transactions,
returning UNFUNDED, FUNDING_IN_PROGRESS, REDEEMED, etc.

Added code to trade-bot to increase robustness. Lots more
changes including unified state change/logging, checking
for existing MESSAGEs, etc.

Added missing websocket methods to silence log noise.

Trade-bot now called per block during synchronization,
instead of per batch, to pick up edge cases where some
potential trade-bot transitions were missed, resulting
in failed trades.

Corresponding changes in Controller, such as notifying
event bus of new block in same thread (thus blocking)
instead of using executor.

Added slightly more robust common block determination
to Synchronizer.

Refactored code in BTC class to use new BitcoinException
rather than simply returning null, with added sub-classes
allowing differentiation between network issues or fund
issues.

Changed BTC.buildSpend to try harder to find UXTOs to
address false "insufficient funds" issues.

Repository change to add index on MessageTransactions
for quicker look-up of trade-related messages.

Reduced reliance on bitcoinj library in BTCP2SH.

Reworked ElectrumX to better detect errors rather than
continuously try more servers to no avail.
Also added genesis block check in case of servers on
different Bitcoin networks.
Now tries to extract upstream bitcoind error codes
and pass those up to caller via exceptions.
Updated list of testnet servers.

MemoryPoW now detects thread interrupt and exits fast.

Moved some non-generic transaction-related repository
methods to their own subclass. For example:
moved TransactionRepository.getMessagesByRecipient
to MessageRepository.getMessagesByParticipants

Updated and added more tests.
2020-09-10 12:03:37 +01:00
catbref
ca3fcc3c67 Tighten up sync status reporting, especially when using forcesync 2020-09-03 12:57:29 +01:00
catbref
de8e5ec920 Updated AdvancedInstaller project file based on v1.3.5 release 2020-09-01 11:08:37 +01:00
catbref
f833e44bd5 Update tools/build-zip.sh to reflect updated start.sh, and also to take optional git tag 2020-09-01 10:41:52 +01:00
catbref
8b0b1db5a4 Improved start-up shell script "start.sh"
Was "run.sh" but renamed to "start.sh" to better complement "stop.sh".
"run.sh" is now a symbolic link to "start.sh"

Reworked Java version check to remove dependency on "bc" tool which
seems not to be installed on some Ubuntu distributions?

Removed -XX:NativeMemoryTracking flag from JVM args.

Fixed incorrect comment regarding java.net.preferIpV4Stack.
Fixed typo in comment.
2020-09-01 10:25:48 +01:00
catbref
5b95f3af02 Bump to v1.3.5 2020-08-31 11:53:12 +01:00
catbref
3cc66609e8 Trial implementation of offline repository periodic maintenance.
Requires node shutdown, lots of time (10s of minutes), spare storage space.
Called via: java -cp qortal.jar org.qortal.RepositoryMaintenance
Not (yet) for general consumption.
2020-08-31 11:51:38 +01:00
catbref
ce468d22dd Fix updating of current tradeoffers list as used by tradeoffers websocket 2020-08-31 11:25:21 +01:00
catbref
3e19516f62 Correct poor synchronization on NTP offset, potentially fixing issue #22 2020-08-31 10:24:10 +01:00
catbref
84dba739d9 Give up on cross-chain trade if initial AT doesn't confirm within 24 hours 2020-08-31 09:21:15 +01:00
catbref
99315c7378 Correct wrong source for lockTimeA when Bob waiting for P2SH-B. Spotted by tcallahan14. In lieu of PR #23 2020-08-31 09:14:15 +01:00
catbref
1ca5b864a9 Repository optimizations!
Added Qortal-side HSQLDB PreparedStatement cache, hashed
by SQL query string, to reduce re-preparing statements.
(HSQLDB actually does the work in avoiding re-preparing
by comparing its own query-to-statement cache map, but we
need to keep an 'open' statement on our side for this to
happen).

Support added for batched INSERT/UPDATE SQL statements to
update many rows in one call.

Several specific repository calls, e.g. modifyMintedBlockCount
or modifyAssetBalance, now have batch versions that allow
many rows to be updated in one call.

In Block, when distributing block rewards, although we still
build a map of balance changes to apply after all calculations,
this map is now handed off wholesale to the repository to
apply in one (or two) queries, instead of a repository call
per account. The balanceChanges map is now keyed by account
address, as opposed to actual Account.

Also in Block, we try to cache the fetched online reward-shares
(typically in Block.isValid et al) to avoid re-fetching them
later when calculating block rewards.

In addition, actually fetching online reward-shares is no longer
done index-by-index, but the whole array of indexes is passed
wholesale to the repository which then returns the corresponding
reward-shares as a list.

In Block.increaseAccountLevels, blocks minted counts are also
updated in one single repository call, rather than one
repository call per account.

When distributing Block rewards to legacy QORA holders,
all necessary info is fetched from the repository in one hit
instead of two-phases of: 1. fetching eligible QORA holders,
and 2. fetching extra data for that QORA holder as needed.

In addition, updated QORT_FROM_QORA asset balances are done
via one batch repository call, rather than per update.
2020-08-26 17:16:45 +01:00
catbref
96eb60dca3 More HSQLDB tests to cover fixes for various HSQLDB issues, especially when using custom HSQLDB build 2020-08-25 17:02:14 +01:00
catbref
c67fcb0034 Updated AdvancedInstaller project file based on v1.3.4 release 2020-08-24 15:40:29 +01:00
catbref
273dfe2365 Bump to v1.3.4 2020-08-24 15:05:51 +01:00
catbref
5952ea4b54 RU translations thanks to Alexander45 2020-08-24 15:04:33 +01:00
catbref
1708ba077c Actually define static constants for BTC fees until dynamic fees happen 2020-08-24 14:28:54 +01:00
catbref
b4301f125d Potential fix for issue #22 2020-08-24 14:27:03 +01:00
catbref
9e52f20f71 Revert back to HSQLDB v2.5.0-fixed until out-of-memory issue located 2020-08-24 14:07:36 +01:00
catbref
31bf388cab BlockMinter (now under org.qortal.controller package) doesn't need full previous block, only previous block data 2020-08-24 14:04:11 +01:00
catbref
276c479a5f Refactor to allow better Bitcoin fee estimation in the future. 2020-08-21 17:37:04 +01:00
catbref
9393689037 Send BTCACCT refunds to first unused received address instead of address derived from tradePrivateKey.
Added BTC.getUnusedReceiveAddress() to support above.
2020-08-21 17:35:33 +01:00
catbref
76485010ad Merge pull request #16 from tcallahan14/feature/electrum_nodes
Updated Electrum nodes list
2020-08-21 13:34:49 +01:00
catbref
b8ac128d5c Improve comparing chains where some blocks signed with cancelled reward-share
Symptoms include this in logs:

Unexpected zero effective minter level for reward-share %s - using 1 instead!

This occurs when Synchronizer compares two sub-chains from a common block,
and one of the blocks is signed by a reward-share key that has
subsequently been cancelled.

Although this is catered for, excessive log-spam is emited.

So in addition to demoting the log level from WARN to DEBUG,
more code has been added to try harder to find the actual data needed,
thus preventing the logging in the first place.

New repository transaction search method added to support above,
along with corresponding tests.
2020-08-21 12:27:06 +01:00
CalDescent
06c75310a1 Updated Electrum nodes list. All nodes have been tested to ensure they respond to jsonrpc calls. 2020-08-15 16:53:09 +01:00
catbref
b9d819220d Bumped HSQLDB to v2.5.1 and AT/cross-chain SQL speed-ups! 2020-08-15 11:12:10 +01:00
catbref
7a569f342f Reduce confusing BlockMinter log spam - issue #9 2020-08-15 10:52:48 +01:00
catbref
f1efae79c8 Speed-ups for some AT-related SQL queries 2020-08-14 11:54:33 +01:00
catbref
1cd4bbc078 Refactored various websockets to event bus from old BlockNotifier/StatusNotifier 2020-08-14 10:03:51 +01:00
catbref
0b5e5832c4 Added another repository deadlock test while investigating a deadlock case 2020-08-14 09:57:08 +01:00
107 changed files with 5899 additions and 1566 deletions

View File

@@ -19,10 +19,10 @@
<ROW Property="Manufacturer" Value="Qortal"/>
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
<ROW Property="NTP_GOOD" Value="false"/>
<ROW Property="ProductCode" Value="1033:{31E3EA92-5348-4D5E-BF92-403470774E62} 2052:{FD291A77-AFE6-4511-BF5C-643D2173F4E5} 2057:{AC09453B-3F19-49E9-984D-BF167F2E29B9} " Type="16"/>
<ROW Property="ProductCode" Value="1033:{3F23DC7A-BC0B-4598-9FD4-C4B927A10D7F} 1049:{CF0D5DDC-7CB7-4308-8F98-DF8D2DB2D38D} 2052:{983B77E5-62CF-431C-B015-B96C5DCA6858} 2057:{BF1C757A-A3A0-4285-906A-6D8D91D74D0A} " Type="16"/>
<ROW Property="ProductLanguage" Value="2057"/>
<ROW Property="ProductName" Value="Qortal"/>
<ROW Property="ProductVersion" Value="1.2.2" Type="32"/>
<ROW Property="ProductVersion" Value="1.3.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"/>
@@ -174,7 +174,7 @@
<ROW Component="ADDITIONAL_LICENSE_INFO_97" ComponentId="{D5544706-E2A7-424F-AEA5-3963E355AA29}" Directory_="jdk.crypto.mscapi_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_97" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_98" ComponentId="{104DBCE8-A458-4B3E-9EFA-2D8613561619}" Directory_="jdk.dynalink_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_98" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_99" ComponentId="{D02E3C37-E81A-48FA-9E28-B26B728AECD9}" Directory_="jdk.httpserver_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_99" Type="0"/>
<ROW Component="AI_CustomARPName" ComponentId="{761A8B87-CEA8-4F5D-82CC-552C48C9EC14}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_CustomARPName" ComponentId="{5FCFB67B-FDD0-4B15-9A58-2188092589E9}" 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="DATA_PATH" ComponentId="{EE0B6107-E244-4CDB-B195-E9038D2F1E0E}" Directory_="DATA_PATH" Attributes="0"/>
@@ -648,7 +648,7 @@
<ROW BootstrOptKey="GlobalOptions" DownloadFolder="[AppDataFolder][|Manufacturer]\[|ProductName]\prerequisites" Options="2"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.BuildComponent">
<ROW BuildKey="DefaultBuild" BuildName="DefaultBuild" BuildOrder="1" BuildType="1" Languages="en_GB;zh;en" LangOpt="1" InstallationType="2" CabsLocation="1" UseLzma="true" LzmaMethod="2" LzmaCompressionLevel="4" PackageType="1" FilesInsideExe="true" ExeIconPath="qortal.ico" ExtractionFolder="[AppDataFolder][|Manufacturer]-extract\[|ProductName] [|ProductVersion]\install" LangsDialog="true" UseLargeSchema="true" Unicode="true" ExeName="[|ProductName]-[|ProductVersion]" MsiPackageType="x64" JRE64Dir="jre64_Dir"/>
<ROW BuildKey="DefaultBuild" BuildName="DefaultBuild" BuildOrder="1" BuildType="1" Languages="en_GB;zh;en;ru" LangOpt="1" InstallationType="2" CabsLocation="1" UseLzma="true" LzmaMethod="2" LzmaCompressionLevel="4" PackageType="1" FilesInsideExe="true" ExeIconPath="qortal.ico" ExtractionFolder="[AppDataFolder][|Manufacturer]-extract\[|ProductName] [|ProductVersion]\install" LangsDialog="true" UseLargeSchema="true" Unicode="true" ExeName="[|ProductName]-[|ProductVersion]" MsiPackageType="x64" JRE64Dir="jre64_Dir"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.DictionaryComponent">
<ROW Path="&lt;AI_DICTS&gt;ui.ail"/>
@@ -656,6 +656,7 @@
<ROW Path="&lt;AI_DICTS&gt;ui_en.ail"/>
<ROW Path="&lt;AI_DICTS&gt;ui_zh.ail"/>
<ROW Path="dictionary.ail" Options="1"/>
<ROW Path="&lt;AI_DICTS&gt;ui_ru.ail"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.FirewallExceptionComponent">
<ROW FirewallException="EXE" DisplayName="[ProductName]" Enabled="1" Scope="*" Condition="((?Qortal.exe=2) AND ($Qortal.exe=3))" Profiles="0" AppPath="[#Qortal.exe]" Port="*" Protocol="ANY"/>
@@ -1128,11 +1129,14 @@
<ROW IniFile="ApplicationType" FileName="Qortal.ini" DirProperty="APPDIR" Section="Application" Key="Application Type" Value="gui" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="ClassPath" FileName="Qortal.ini" DirProperty="APPDIR" Section="Class Path" Key="Class Path" Value="[#qortal.jar];" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="FailureCheck" FileName="Qortal.ini" DirProperty="APPDIR" Section="Application" Key="Failure Check" Value="yes" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="JVMSource" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="JVM Source" Value="favor_JDK" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="JVMType" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="JVM Type" Value="favor_server" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="MainClass" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="Main Class" Value="org.qortal.controller.Controller" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="MaximumVersion" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="Maximum Version" Value="any" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="MinimumVersion" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="Minimum Version" Value="11" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="OverrideWorkingDir" FileName="Qortal.ini" DirProperty="APPDIR" Section="Application" Key="Override WorkingDir" Value="yes" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="SingleInstance" FileName="Qortal.ini" DirProperty="APPDIR" Section="Application" Key="Single Instance" Value="yes" Action="0" Component_="Qortal.exe"/>
<ROW IniFile="VMProvider" FileName="Qortal.ini" DirProperty="APPDIR" Section="Java Runtime Environment" Key="VM Provider" Value="any" Action="0" Component_="Qortal.exe"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiInstExSeqComponent">
<ROW Action="AI_DOWNGRADE" Condition="AI_NEWERPRODUCTFOUND AND (UILevel &lt;&gt; 5)" Sequence="210"/>
@@ -1189,7 +1193,7 @@
<ROW Action="Set_DATA_PATH_property" Condition="( NOT Installed )" Sequence="77"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiJavaComponent">
<ROW Name="Qortal.exe" Launcher="Qortal.exe" MainClass="MainClass" ClassPath="ClassPath" JREMin="MinimumVersion" JREMax="MaximumVersion" IconPath="qortal.ico" AppType="ApplicationType" SingleInstance="SingleInstance" PlusList="APPDIR;" MinusList="#Qortal.exe;" MacDescription="[|CurrentJavaProductName] [|ProductVersion] © [|Manufacturer], Inc, 2019" MacBundleId="com.[|Manufacturer].[|ProductName].[|CurrentJavaProductName]" FailureCheck="FailureCheck" OverrideWkDir="OverrideWorkingDir" UACExecutionLevel="0" Platform64="true"/>
<ROW Name="Qortal.exe" Launcher="Qortal.exe" MainClass="MainClass" ClassPath="ClassPath" JREMin="MinimumVersion" JREMax="MaximumVersion" IconPath="qortal.ico" AppType="ApplicationType" JVMType="JVMType" SingleInstance="SingleInstance" PlusList="APPDIR;" MinusList="#Qortal.exe;" MacDescription="[|CurrentJavaProductName] [|ProductVersion] © [|Manufacturer], Inc, 2019" MacBundleId="com.[|Manufacturer].[|ProductName].[|CurrentJavaProductName]" JVMSourcePreference="JVMSource" FailureCheck="FailureCheck" OverrideWkDir="OverrideWorkingDir" UACExecutionLevel="0" VMProviderOptions="VMProvider" Platform64="true"/>
</COMPONENT>
<COMPONENT cid="caphyon.advinst.msicomp.MsiLaunchConditionsComponent">
<ROW Condition="( Version9X OR VersionNT64 )" Description="[ProductName] can not be installed on [WindowsTypeNTDisplay]." DescriptionLocId="AI.LaunchCondition.NoNT" IsPredefined="true" Builds="DefaultBuild"/>

20
WindowsInstaller/dictionary.ail Executable file → Normal file
View File

@@ -3,95 +3,115 @@
<!-- Control table -->
<ENTRY id="Control.Text.CustomizeDataPathDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="ru" value="Вы можете выбрать место хранения блокчейна и других данных."/>
<STRING lang="zh" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
<STRING lang="zh_TW" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDataPathDlg#Text">
<STRING lang="en" value="Select one of the options below, then click &quot;Next&quot;."/>
<STRING lang="ru" value="Выберите один из вариантов ниже, затем нажмите Далее"/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
<STRING lang="zh_TW" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDataPathDlg#Title">
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
<STRING lang="ru" value="Выберите место хранения данных."/>
<STRING lang="zh" value="选择数据保存的文件夹?"/>
<STRING lang="zh_TW" value="选择数据保存的文件夹?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDbDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="ru" value="Вы можете выбрать место хранения блокчейна и других данных."/>
<STRING lang="zh" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh_TW" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDbDlg#Title">
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
<STRING lang="ru" value="Выберите место хранения данных."/>
<STRING lang="zh" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh_TW" value="Choose Custom Data Storage Folder?"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Description">
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="ru" value="Это папка, в которой будет храниться блокчейн и другие данные."/>
<STRING lang="zh" value="这里是区块链及其它数据存放的文件夹"/>
<STRING lang="zh_TW" value="这里是区块链及其它数据存放的文件夹"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Text">
<STRING lang="en" value="To store data in this folder, click &quot;[Text_Next]&quot;. To store data in a different folder, enter it below or click &quot;Browse&quot;."/>
<STRING lang="ru" value="Чтобы сохранить данные в этой папке, нажмите &quot;[Text_Next]&quot;. Чтобы сохранить данные в другой папке, введите ее ниже или нажмите &quot;Обзор&quot;."/>
<STRING lang="zh" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
<STRING lang="zh_TW" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Title">
<STRING lang="en" value="Select Data Storage Folder"/>
<STRING lang="ru" value="Выберите папку для хранения данных"/>
<STRING lang="zh" value="请选择文件存储地方"/>
<STRING lang="zh_TW" value="请选择文件存储地方"/>
</ENTRY>
<ENTRY id="Control.Text.DbFolderDlg#Description">
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="ru" value="Это папка, в которой будет храниться блокчейн и другие данные."/>
<STRING lang="zh" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh_TW" value="This is the folder where the blockchain, and other data, will be stored."/>
</ENTRY>
<ENTRY id="Control.Text.DbFolderDlg#Title">
<STRING lang="en" value="Select Data Storage Folder"/>
<STRING lang="ru" value="Выберите папку для хранения данных"/>
<STRING lang="zh" value="请选择文件存储地方"/>
<STRING lang="zh_TW" value="请选择文件存储地方"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Description">
<STRING lang="en" value="Reconfigure Windows for more accurate time?"/>
<STRING lang="ru" value="Настроить синхронизацию времени системы Windows?"/>
<STRING lang="zh" value="重新配置Windows以获得更准确的时间"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_1">
<STRING lang="en" value="An accurate Windows clock is required to connect to the [ProductName] network and make transactions."/>
<STRING lang="ru" value="Для подключения к сети Qortal и совершения транзакций требуется точная настройка времени Windows"/>
<STRING lang="zh" value="需要准确的Windows时钟才能连接到[ProductName]网络并进行交易。"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_2">
<STRING lang="en" value="Select one of the options below, then click &quot;Next&quot;."/>
<STRING lang="ru" value="Выберите один из вариантов ниже, затем нажмите"/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_3">
<STRING lang="en" value="Your computer&apos;s clock needs to be accurate to within 0.5 seconds."/>
<STRING lang="ru" value="Точность времени вашего компьютера должна составлять 0.5 секунд."/>
<STRING lang="zh" value="您的计算机时钟需要准确到0.5秒内。"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Title">
<STRING lang="en" value="Windows clock accuracy"/>
<STRING lang="ru" value="Настройка времени системы Windows"/>
<STRING lang="zh" value="Windows 时钟精度"/>
</ENTRY>
<ENTRY id="Control.Text.VerifyRemoveDlg#RemoveBlockchainCheckbox">
<STRING lang="en" value="Remove downloaded blockchain and other data"/>
<STRING lang="ru" value="Удалить загруженный блокчейн и другие данные"/>
<STRING lang="zh" value="删除您下载的区块链"/>
</ENTRY>
<!-- RadioButton table -->
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#choose">
<STRING lang="en" value="Choose custom data storage folder..."/>
<STRING lang="ru" value="Выбрать папку для хранения данных..."/>
<STRING lang="zh" value="选择特定的文件夹存储"/>
<STRING lang="zh_TW" value="选择特定的文件夹存储"/>
</ENTRY>
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#default">
<STRING lang="en" value="Use default location "/>
<STRING lang="ru" value="Использовать папку по умолчанию"/>
<STRING lang="zh" value="使用默认存储地点"/>
<STRING lang="zh_TW" value="使用默认存储地点"/>
</ENTRY>
<ENTRY id="RadioButton.Text.RECONFIG_NTP#1">
<STRING lang="en" value="Yes, configure Windows to use internet time servers (Recommended)"/>
<STRING lang="ru" value="Да, настроить синхронизацию времени Windows (Рекомендуется)"/>
<STRING lang="zh" value="是将Windows配置为使用多个Internet时间服务器 (推荐的)"/>
</ENTRY>
<ENTRY id="RadioButton.Text.RECONFIG_NTP#2">
<STRING lang="en" value="No, I will manage clock accuracy myself"/>
<STRING lang="ru" value="Нет, я сам буду управлять настройками часов"/>
<STRING lang="zh" value="不,我会自己管理时钟精度。"/>
</ENTRY>
</DICTIONARY>

Binary file not shown.

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<version>1.3.8</version>
<description>POM was created from install:install-file</description>
</project>

View File

@@ -3,13 +3,14 @@
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<versioning>
<release>1.3.7</release>
<release>1.3.8</release>
<versions>
<version>1.3.4</version>
<version>1.3.5</version>
<version>1.3.6</version>
<version>1.3.7</version>
<version>1.3.8</version>
</versions>
<lastUpdated>20200812131412</lastUpdated>
<lastUpdated>20200925114415</lastUpdated>
</versioning>
</metadata>

13
pom.xml
View File

@@ -3,19 +3,18 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.3.3</version>
<version>1.3.7</version>
<packaging>jar</packaging>
<properties>
<bitcoinj.version>0.15.5</bitcoinj.version>
<bouncycastle.version>1.64</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.3.7</ciyam-at.version>
<ciyam-at.version>1.3.8</ciyam-at.version>
<commons-net.version>3.6</commons-net.version>
<commons-text.version>1.8</commons-text.version>
<dagger.version>1.2.2</dagger.version>
<guava.version>28.1-jre</guava.version>
<hsqldb.version>2.5.0-fixed</hsqldb.version>
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
<hsqldb.version>2.5.1</hsqldb.version>
<jersey.version>2.29.1</jersey.version>
<jetty.version>9.4.29.v20200521</jetty.version>
<log4j.version>2.12.1</log4j.version>
@@ -397,12 +396,6 @@
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>sqltool</artifactId>
<version>${hsqldb-sqltool.version}</version>
<scope>test</scope>
</dependency>
<!-- CIYAM AT (automated transactions) -->
<dependency>
<groupId>org.ciyam</groupId>

51
run.sh
View File

@@ -1,51 +0,0 @@
#!/bin/sh
# There's no need to run as root, so don't allow it, for security reasons
if [ "$USER" = "root" ]; then
echo "Please su to non-root user before running"
exit
fi
# Validate Java is installed and the minimum version is available
MIN_JAVA_VER='11'
if command -v java > /dev/null 2>&1; then
version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
version=$(echo $version | cut -d'.' -f1,2)
if [ `echo "${version}>=${MIN_JAVA_VER}" | bc` -eq 1 ]; then
echo 'Passed Java version check'
else
echo 'Please upgrade your Java to version 11 or greater'
exit 1
fi
else
echo 'Java is not available, please install Java 11 or greater'
exit 1
fi
# No qortal.jar but we have a Maven built one?
# Be helpful and copy across to correct location
if [ ! -e qortal.jar -a -f target/qortal*.jar ]; then
echo "Copying Maven-built Qortal JAR to correct pathname"
cp target/qortal*.jar qortal.jar
fi
# Limits Java JVM stack size and maximum heap usage.
# Comment out for bigger systems, e.g. non-routers
# or when API documentation is enabled
# JVM_MEMORY_ARGS="-Xss256k -Xmx128m"
# Although java.net.preferIPv4Stack is supposed to be false
# by default in Java 11, on some platforms (e.g. FreeBSD 12),
# it is overriden to be true by default. Hence we explicitly
# set it to true to obtain desired behaviour.
nohup nice -n 20 java \
-Djava.net.preferIPv4Stack=false \
-XX:NativeMemoryTracking=summary \
${JVM_MEMORY_ARGS} \
-jar qortal.jar \
1>run.log 2>&1 &
# Save backgrounded process's PID
echo $! > run.pid
echo qortal running as pid $!

1
run.sh Symbolic link
View File

@@ -0,0 +1 @@
start.sh

View File

@@ -0,0 +1,75 @@
package org.qortal;
import java.security.Security;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.controller.Controller;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
public class RepositoryMaintenance {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static final Logger LOGGER = LogManager.getLogger(RepositoryMaintenance.class);
public static void main(String[] args) {
LOGGER.info("Repository maintenance starting up...");
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
// Load/check settings, which potentially sets up blockchain config, etc.
try {
if (args.length > 0)
Settings.fileInstance(args[0]);
else
Settings.getInstance();
} catch (Throwable t) {
LOGGER.error("Settings file error: " + t.getMessage());
System.exit(2);
}
LOGGER.info("Opening repository");
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
// If exception has no cause then repository is in use by some other process.
if (e.getCause() == null) {
LOGGER.info("Repository in use by another process?");
} else {
LOGGER.error("Unable to start repository", e);
}
System.exit(1);
}
LOGGER.info("Starting repository periodic maintenance. This can take a while...");
try (final Repository repository = RepositoryManager.getRepository()) {
repository.performPeriodicMaintenance();
LOGGER.info("Repository periodic maintenance completed");
} catch (DataException e) {
LOGGER.error("Repository periodic maintenance failed", e);
}
try {
LOGGER.info("Shutting down repository");
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
LOGGER.error("Error occurred while shutting down repository", e);
}
}
}

View File

@@ -5,10 +5,20 @@ import java.net.UnknownHostException;
import javax.servlet.http.HttpServletRequest;
public class Security {
import org.qortal.settings.Settings;
public abstract class Security {
public static final String API_KEY_HEADER = "X-API-KEY";
// TODO: replace with proper authentication
public static void checkApiCallAllowed(HttpServletRequest request) {
String expectedApiKey = Settings.getInstance().getApiKey();
String passedApiKey = request.getHeader(API_KEY_HEADER);
if ((expectedApiKey != null && !expectedApiKey.equals(passedApiKey)) ||
(passedApiKey != null && !passedApiKey.equals(expectedApiKey)))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
InetAddress remoteAddr;
try {
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
@@ -19,4 +29,5 @@ public class Security {
if (!remoteAddr.isLoopbackAddress())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
}
}

View File

@@ -1,5 +1,6 @@
package org.qortal.api.model;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
@@ -13,17 +14,61 @@ import org.qortal.transaction.Transaction.TransactionType;
@XmlAccessorType(XmlAccessType.FIELD)
public class ActivitySummary {
public int blockCount;
public int transactionCount;
public int assetsIssued;
public int namesRegistered;
private int blockCount;
private int assetsIssued;
private int namesRegistered;
// Assuming TransactionType values are contiguous so 'length' equals count
@XmlJavaTypeAdapter(TransactionCountMapXmlAdapter.class)
public Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
private Map<TransactionType, Integer> transactionCountByType = new EnumMap<>(TransactionType.class);
private int totalTransactionCount = 0;
public ActivitySummary() {
// Needed for JAXB
}
public int getBlockCount() {
return this.blockCount;
}
public void setBlockCount(int blockCount) {
this.blockCount = blockCount;
}
public int getTotalTransactionCount() {
return this.totalTransactionCount;
}
public int getAssetsIssued() {
return this.assetsIssued;
}
public void setAssetsIssued(int assetsIssued) {
this.assetsIssued = assetsIssued;
}
public int getNamesRegistered() {
return this.namesRegistered;
}
public void setNamesRegistered(int namesRegistered) {
this.namesRegistered = namesRegistered;
}
public Map<TransactionType, Integer> getTransactionCountByType() {
return Collections.unmodifiableMap(this.transactionCountByType);
}
public void setTransactionCountByType(TransactionType transactionType, int transactionCount) {
this.transactionCountByType.put(transactionType, transactionCount);
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
}
public void setTransactionCountByType(Map<TransactionType, Integer> transactionCountByType) {
this.transactionCountByType = new EnumMap<>(transactionCountByType);
this.totalTransactionCount = this.transactionCountByType.values().stream().mapToInt(Integer::intValue).sum();
}
}

View File

@@ -22,6 +22,9 @@ public class CrossChainBitcoinRefundRequest {
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
public BigDecimal bitcoinMinerFee;
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
public byte[] receivingAccountInfo;
public CrossChainBitcoinRefundRequest() {
}

View File

@@ -83,4 +83,10 @@ public class CrossChainOfferSummary {
return this.partnerQortalReceivingAddress;
}
// For debugging mostly
public String toString() {
return String.format("%s: %s", this.qortalAtAddress, this.mode.name());
}
}

View File

@@ -11,6 +11,7 @@ public class NodeInfo {
public String buildVersion;
public long buildTimestamp;
public String nodeId;
public boolean isTestNet;
public NodeInfo() {
}

View File

@@ -20,17 +20,14 @@ public class NodeStatus {
public final int height;
public NodeStatus() {
isMintingPossible = Controller.getInstance().isMintingPossible();
isSynchronizing = Controller.getInstance().isSynchronizing();
this.isMintingPossible = Controller.getInstance().isMintingPossible();
if (isSynchronizing)
syncPercent = Controller.getInstance().getSyncPercent();
else
syncPercent = null;
this.syncPercent = Controller.getInstance().getSyncPercent();
this.isSynchronizing = this.syncPercent != null;
numberOfConnections = Network.getInstance().getHandshakedPeers().size();
this.numberOfConnections = Network.getInstance().getHandshakedPeers().size();
height = Controller.getInstance().getChainHeight();
this.height = Controller.getInstance().getChainHeight();
}
}

View File

@@ -8,13 +8,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotRespondRequest {
@Schema(description = "Qortal AT address", example = "AH3e3jHEsGHPVQPDiJx4pYqgVi72auxgVy")
@Schema(description = "Qortal AT address", example = "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
public String atAddress;
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
@Schema(description = "Bitcoin BIP32 extended private key", example = "xprv___________________________________________________________________________________________________________")
public String xprv58;
@Schema(description = "Qortal address for receiving QORT from AT")
@Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
public String receivingAddress;
public TradeBotRespondRequest() {

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
@@ -473,6 +474,7 @@ public class AddressesResource {
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String computePublicize(String rawBytes58) {
Security.checkApiCallAllowed(request);

View File

@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
@@ -40,7 +41,6 @@ import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ActivitySummary;
@@ -57,6 +57,7 @@ import org.qortal.network.PeerAddress;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@@ -118,6 +119,7 @@ public class AdminResource {
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
return nodeInfo;
}
@@ -132,6 +134,7 @@ public class AdminResource {
)
}
)
@SecurityRequirement(name = "apiKey")
public NodeStatus status() {
Security.checkApiCallAllowed(request);
@@ -152,6 +155,7 @@ public class AdminResource {
)
}
)
@SecurityRequirement(name = "apiKey")
public String shutdown() {
Security.checkApiCallAllowed(request);
@@ -180,7 +184,10 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public ActivitySummary summary() {
Security.checkApiCallAllowed(request);
ActivitySummary summary = new ActivitySummary();
LocalDate date = LocalDate.now();
@@ -192,16 +199,13 @@ public class AdminResource {
int startHeight = repository.getBlockRepository().getHeightFromTimestamp(start);
int endHeight = repository.getBlockRepository().getBlockchainHeight();
summary.blockCount = endHeight - startHeight;
summary.setBlockCount(endHeight - startHeight);
summary.transactionCountByType = repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight);
summary.setTransactionCountByType(repository.getTransactionRepository().getTransactionSummary(startHeight + 1, endHeight));
for (Integer count : summary.transactionCountByType.values())
summary.transactionCount += count;
summary.setAssetsIssued(repository.getAssetRepository().getRecentAssetIds(start).size());
summary.assetsIssued = repository.getAssetRepository().getRecentAssetIds(start).size();
summary.namesRegistered = repository.getNameRepository().getRecentNames(start).size();
summary.setNamesRegistered (repository.getNameRepository().getRecentNames(start).size());
return summary;
} catch (DataException e) {
@@ -209,6 +213,30 @@ public class AdminResource {
}
}
@GET
@Path("/enginestats")
@Operation(
summary = "Fetch statistics snapshot for core engine",
responses = {
@ApiResponse(
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
array = @ArraySchema(
schema = @Schema(
implementation = Controller.StatsSnapshot.class
)
)
)
)
}
)
@SecurityRequirement(name = "apiKey")
public Controller.StatsSnapshot getEngineStats() {
Security.checkApiCallAllowed(request);
return Controller.getInstance().getStatsSnapshot();
}
@GET
@Path("/mintingaccounts")
@Operation(
@@ -221,6 +249,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<MintingAccountData> getMintingAccounts() {
Security.checkApiCallAllowed(request);
@@ -267,6 +296,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE, ApiError.CANNOT_MINT})
@SecurityRequirement(name = "apiKey")
public String addMintingAccount(String seed58) {
Security.checkApiCallAllowed(request);
@@ -319,6 +349,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String deleteMintingAccount(String key58) {
Security.checkApiCallAllowed(request);
@@ -418,6 +449,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String orphan(String targetHeightString) {
Security.checkApiCallAllowed(request);
@@ -435,8 +467,6 @@ public class AdminResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
} catch (ApiException e) {
throw e;
}
}
@@ -461,6 +491,7 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String forceSync(String targetPeerAddress) {
Security.checkApiCallAllowed(request);
@@ -492,8 +523,6 @@ public class AdminResource {
return syncResult.name();
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (ApiException e) {
throw e;
} catch (UnknownHostException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (InterruptedException e) {
@@ -501,4 +530,151 @@ public class AdminResource {
}
}
@GET
@Path("/repository/data")
@Operation(
summary = "Export sensitive/node-local data from repository.",
description = "Exports data to .script files on local machine"
)
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String exportRepository() {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.exportNodeLocalData();
return "true";
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform export
return "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/repository/data")
@Operation(
summary = "Import data into repository.",
description = "Imports data from file on local machine. Filename is forced to 'import.script' if apiKey is not set.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "MintingAccounts.script"
)
)
),
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String importRepository(String filename) {
Security.checkApiCallAllowed(request);
// Hard-coded because it's too dangerous to allow user-supplied filenames in weaker security contexts
if (Settings.getInstance().getApiKey() == null)
filename = "import.script";
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.importDataFromFile(filename);
repository.saveChanges();
return "true";
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform import
return "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/repository/checkpoint")
@Operation(
summary = "Checkpoint data in repository.",
description = "Forces repository to checkpoint uncommitted writes.",
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String checkpointRepository() {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.checkpoint(true);
repository.saveChanges();
return "true";
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform checkpoint
return "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@DELETE
@Path("/repository")
@Operation(
summary = "Perform maintenance on repository.",
description = "Requires enough free space to rebuild repository. This will pause your node for a while."
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public void performRepositoryMaintenance() {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.performPeriodicMaintenance();
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// No big deal
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -1,11 +1,17 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.qortal.api.Security;
@OpenAPIDefinition(
info = @Info( title = "Qortal API", description = "NOTE: byte-arrays are encoded in Base58" ),
tags = {
@@ -30,5 +36,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
})
}
)
@SecuritySchemes({
@SecurityScheme(name = "basicAuth", type = SecuritySchemeType.HTTP, scheme = "basic"),
@SecurityScheme(name = "apiKey", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, paramName = Security.API_KEY_HEADER)
})
public class ApiDefinition {
}

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
@@ -156,6 +157,7 @@ public class ChatResource {
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildChat(ChatTransactionData transactionData) {
Security.checkApiCallAllowed(request);
@@ -203,6 +205,7 @@ public class ChatResource {
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildChat(String rawBytes58) {
Security.checkApiCallAllowed(request);

View File

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
@@ -58,6 +59,7 @@ import org.qortal.controller.TradeBot;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
@@ -154,6 +156,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildTrade(CrossChainBuildRequest tradeRequest) {
Security.checkApiCallAllowed(request);
@@ -249,6 +252,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildTradeMessage(CrossChainTradeRequest tradeRequest) {
Security.checkApiCallAllowed(request);
@@ -332,6 +336,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildRedeemMessage(CrossChainSecretRequest secretRequest) {
Security.checkApiCallAllowed(request);
@@ -403,6 +408,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String buildCancelMessage(CrossChainCancelRequest cancelRequest) {
Security.checkApiCallAllowed(request);
@@ -458,6 +464,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String deriveP2shA(CrossChainBitcoinTemplateRequest templateRequest) {
Security.checkApiCallAllowed(request);
@@ -484,6 +491,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String deriveP2shB(CrossChainBitcoinTemplateRequest templateRequest) {
Security.checkApiCallAllowed(request);
@@ -541,6 +549,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public CrossChainBitcoinP2SHStatus checkP2shA(CrossChainBitcoinTemplateRequest templateRequest) {
Security.checkApiCallAllowed(request);
@@ -567,6 +576,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public CrossChainBitcoinP2SHStatus checkP2shB(CrossChainBitcoinTemplateRequest templateRequest) {
Security.checkApiCallAllowed(request);
@@ -602,17 +612,12 @@ public class CrossChainResource {
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
if (medianBlockTime == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
int medianBlockTime = BTC.getInstance().getMedianBlockTime();
long now = NTP.getTime();
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
CrossChainBitcoinP2SHStatus p2shStatus = new CrossChainBitcoinP2SHStatus();
p2shStatus.bitcoinP2shAddress = p2shAddress.toString();
@@ -634,6 +639,8 @@ public class CrossChainResource {
return p2shStatus;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (BitcoinException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
}
}
@@ -658,6 +665,7 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String refundP2shA(CrossChainBitcoinRefundRequest refundRequest) {
Security.checkApiCallAllowed(request);
@@ -685,6 +693,7 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String refundP2shB(CrossChainBitcoinRefundRequest refundRequest) {
Security.checkApiCallAllowed(request);
@@ -719,6 +728,13 @@ public class CrossChainResource {
if (refundRequest.atAddress == null || !Crypto.isValidAtAddress(refundRequest.atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (refundRequest.receivingAccountInfo == null)
refundRequest.receivingAccountInfo = refundKey.getPubKeyHash();
if (refundRequest.receivingAccountInfo.length != 20)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
// Extract data from cross-chain trading AT
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress);
@@ -739,9 +755,7 @@ public class CrossChainResource {
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs.isEmpty())
@@ -756,15 +770,15 @@ public class CrossChainResource {
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundRequest.receivingAccountInfo);
BTC.getInstance().broadcastTransaction(refundTransaction);
if (!wasBroadcast)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
return refundTransaction.getTxId().toString();
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (BitcoinException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
}
}
@@ -790,6 +804,7 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String redeemP2shA(CrossChainBitcoinRedeemRequest redeemRequest) {
Security.checkApiCallAllowed(request);
@@ -818,6 +833,7 @@ public class CrossChainResource {
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN,
ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String redeemP2shB(CrossChainBitcoinRedeemRequest redeemRequest) {
Security.checkApiCallAllowed(request);
@@ -877,16 +893,12 @@ public class CrossChainResource {
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
if (medianBlockTime == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
int medianBlockTime = BTC.getInstance().getMedianBlockTime();
long now = NTP.getTime();
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
if (p2shBalance < crossChainTradeData.expectedBitcoin)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
@@ -902,14 +914,14 @@ public class CrossChainResource {
Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue());
org.bitcoinj.core.Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret, redeemRequest.receivingAccountInfo);
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
if (!wasBroadcast)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
BTC.getInstance().broadcastTransaction(redeemTransaction);
return redeemTransaction.getTxId().toString();
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (BitcoinException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
}
}
@@ -936,6 +948,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY})
@SecurityRequirement(name = "apiKey")
public String getBitcoinWalletBalance(String xprv58) {
Security.checkApiCallAllowed(request);
@@ -970,6 +983,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String sendBitcoin(BitcoinSendRequest bitcoinSendRequest) {
Security.checkApiCallAllowed(request);
@@ -994,8 +1008,11 @@ public class CrossChainResource {
if (spendTransaction == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
if (!BTC.getInstance().broadcastTransaction(spendTransaction))
try {
BTC.getInstance().broadcastTransaction(spendTransaction);
} catch (BitcoinException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
}
return "true";
}
@@ -1017,6 +1034,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<TradeBotData> getTradeBotStates() {
Security.checkApiCallAllowed(request);
@@ -1047,6 +1065,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
Security.checkApiCallAllowed(request);
@@ -1102,6 +1121,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String tradeBotResponder(TradeBotRespondRequest tradeBotRespondRequest) {
Security.checkApiCallAllowed(request);
@@ -1166,6 +1186,7 @@ public class CrossChainResource {
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public String tradeBotDelete(String tradePrivateKey58) {
Security.checkApiCallAllowed(request);

View File

@@ -6,8 +6,11 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -26,10 +29,17 @@ import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ConnectedPeer;
import org.qortal.controller.Controller;
import org.qortal.controller.Synchronizer;
import org.qortal.controller.Synchronizer.SynchronizationResult;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.PeerAddress;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.NTP;
@@ -122,6 +132,7 @@ public class PeersResource {
)
}
)
@SecurityRequirement(name = "apiKey")
public ExecuteProduceConsume.StatsSnapshot getEngineStats() {
Security.checkApiCallAllowed(request);
@@ -159,6 +170,7 @@ public class PeersResource {
@ApiErrors({
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
})
@SecurityRequirement(name = "apiKey")
public String addPeer(String address) {
Security.checkApiCallAllowed(request);
@@ -213,6 +225,7 @@ public class PeersResource {
@ApiErrors({
ApiError.INVALID_NETWORK_ADDRESS, ApiError.REPOSITORY_ISSUE
})
@SecurityRequirement(name = "apiKey")
public String removePeer(String address) {
Security.checkApiCallAllowed(request);
@@ -248,6 +261,7 @@ public class PeersResource {
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
@SecurityRequirement(name = "apiKey")
public String removeKnownPeers(String address) {
Security.checkApiCallAllowed(request);
@@ -260,4 +274,68 @@ public class PeersResource {
}
}
@POST
@Path("/commonblock")
@Operation(
summary = "Report common block with given peer.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "node2.qortal.org"
)
)
),
responses = {
@ApiResponse(
description = "the block",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockSummaryData.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<BlockSummaryData> commonBlock(String targetPeerAddress) {
Security.checkApiCallAllowed(request);
try {
// Try to resolve passed address to make things easier
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress);
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress();
List<Peer> peers = Network.getInstance().getHandshakedPeers();
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().equals(resolvedAddress)).findFirst().orElse(null);
if (targetPeer == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
try (final Repository repository = RepositoryManager.getRepository()) {
int ourInitialHeight = Controller.getInstance().getChainHeight();
boolean force = true;
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
SynchronizationResult findCommonBlockResult = Synchronizer.getInstance().fetchSummariesFromCommonBlock(repository, targetPeer, ourInitialHeight, force, peerBlockSummaries);
if (findCommonBlockResult != SynchronizationResult.OK)
return null;
return peerBlockSummaries;
}
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (UnknownHostException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (InterruptedException e) {
return null;
}
}
}

View File

@@ -31,6 +31,7 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, String> pathParams = getPathParams(session, "/{address}");
@@ -49,16 +50,19 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {

View File

@@ -13,59 +13,87 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.NodeStatus;
import org.qortal.controller.StatusNotifier;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.controller.Controller;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
@WebSocket
@SuppressWarnings("serial")
public class AdminStatusWebSocket extends ApiWebSocket {
public class AdminStatusWebSocket extends ApiWebSocket implements Listener {
private static final AtomicReference<String> previousOutput = new AtomicReference<>(null);
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(AdminStatusWebSocket.class);
try {
previousOutput.set(buildStatusString());
} catch (IOException e) {
// How to fail properly?
return;
}
EventBus.INSTANCE.addListener(this::listen);
}
@Override
public void listen(Event event) {
if (!(event instanceof Controller.StatusChangeEvent))
return;
String newOutput;
try {
newOutput = buildStatusString();
} catch (IOException e) {
// Ignore this time?
return;
}
if (previousOutput.getAndUpdate(currentValue -> newOutput).equals(newOutput))
// Output hasn't changed, so don't send anything
return;
for (Session session : getSessions())
this.sendStatus(session, newOutput);
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
AtomicReference<String> previousOutput = new AtomicReference<>(null);
this.sendStatus(session, previousOutput.get());
StatusNotifier.Listener listener = timestamp -> onNotify(session, previousOutput);
StatusNotifier.getInstance().register(session, listener);
this.onNotify(session, previousOutput);
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
StatusNotifier.getInstance().deregister(session);
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* We ignore errors for now, but method here to silence log spam */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session,AtomicReference<String> previousOutput) {
try (final Repository repository = RepositoryManager.getRepository()) {
NodeStatus nodeStatus = new NodeStatus();
private static String buildStatusString() throws IOException {
NodeStatus nodeStatus = new NodeStatus();
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, nodeStatus);
return stringWriter.toString();
}
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, nodeStatus);
// Only output if something has changed
String output = stringWriter.toString();
if (output.equals(previousOutput.get()))
return;
previousOutput.set(output);
session.getRemote().sendStringByFuture(output);
} catch (DataException | IOException | WebSocketException e) {
private void sendStatus(Session session, String status) {
try {
session.getRemote().sendStringByFuture(status);
} catch (WebSocketException e) {
// No output this time?
}
}

View File

@@ -14,7 +14,11 @@ import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.ApiError;
import org.qortal.api.model.BlockInfo;
import org.qortal.controller.BlockNotifier;
import org.qortal.controller.Controller;
import org.qortal.data.block.BlockData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -22,26 +26,42 @@ import org.qortal.utils.Base58;
@WebSocket
@SuppressWarnings("serial")
public class BlocksWebSocket extends ApiWebSocket {
public class BlocksWebSocket extends ApiWebSocket implements Listener {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(BlocksWebSocket.class);
EventBus.INSTANCE.addListener(this::listen);
}
@Override
public void listen(Event event) {
if (!(event instanceof Controller.NewBlockEvent))
return;
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
BlockInfo blockInfo = new BlockInfo(blockData);
for (Session session : getSessions())
sendBlockInfo(session, blockInfo);
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo);
BlockNotifier.getInstance().register(session, listener);
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
BlockNotifier.getInstance().deregister(session);
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* We ignore errors for now, but method here to silence log spam */
}
@OnWebSocketMessage
@@ -71,7 +91,7 @@ public class BlocksWebSocket extends ApiWebSocket {
return;
}
onNotify(session, blockInfos.get(0));
sendBlockInfo(session, blockInfos.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
@@ -100,13 +120,13 @@ public class BlocksWebSocket extends ApiWebSocket {
return;
}
onNotify(session, blockInfos.get(0));
sendBlockInfo(session, blockInfos.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
}
private void onNotify(Session session, BlockInfo blockInfo) {
private void sendBlockInfo(Session session, BlockInfo blockInfo) {
StringWriter stringWriter = new StringWriter();
try {

View File

@@ -32,6 +32,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
@@ -86,16 +87,19 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {

View File

@@ -11,6 +11,7 @@ import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@@ -71,6 +72,7 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
// Send all known trade-bot entries
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -92,10 +94,16 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */

View File

@@ -6,20 +6,27 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.CrossChainOfferSummary;
import org.qortal.controller.BlockNotifier;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTCACCT;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -27,120 +34,55 @@ import org.qortal.utils.NTP;
@WebSocket
@SuppressWarnings("serial")
public class TradeOffersWebSocket extends ApiWebSocket {
public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
private static final Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class);
private static final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
// OFFERING
private static final Map<String, CrossChainOfferSummary> currentSummaries = new HashMap<>();
// REDEEMED/REFUNDED/CANCELLED
private static final Map<String, CrossChainOfferSummary> historicSummaries = new HashMap<>();
private static final Predicate<CrossChainOfferSummary> isHistoric = offerSummary
-> offerSummary.getMode() == BTCACCT.Mode.REDEEMED
|| offerSummary.getMode() == BTCACCT.Mode.REFUNDED
|| offerSummary.getMode() == BTCACCT.Mode.CANCELLED;
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(TradeOffersWebSocket.class);
try (final Repository repository = RepositoryManager.getRepository()) {
populateCurrentSummaries(repository);
populateHistoricSummaries(repository);
} catch (DataException e) {
// How to fail properly?
return;
}
EventBus.INSTANCE.addListener(this::listen);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
@Override
public void listen(Event event) {
if (!(event instanceof Controller.NewBlockEvent))
return;
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
// Process any new info
List<CrossChainOfferSummary> crossChainOfferSummaries;
try (final Repository repository = RepositoryManager.getRepository()) {
List<ATStateData> initialAtStates;
// We want ALL OFFERING trades
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET;
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value;
Integer minimumFinalHeight = null;
initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (initialAtStates == null) {
session.close(4001, "repository issue fetching OFFERING trades");
return;
}
// Save initial AT modes
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
// Convert to offer summaries
crossChainOfferSummaries = produceSummaries(repository, initialAtStates, null);
if (includeHistoric) {
// We also want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
long timestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
if (minimumFinalHeight != 0) {
isFinished = Boolean.TRUE;
dataByteOffset = null;
expectedValue = null;
++minimumFinalHeight; // because height is just *before* timestamp
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (historicAtStates == null) {
session.close(4002, "repository issue fetching historic trades");
return;
}
for (ATStateData historicAtState : historicAtStates) {
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
switch (historicOfferSummary.getMode()) {
case REDEEMED:
case REFUNDED:
case CANCELLED:
break;
default:
continue;
}
// Add summary to initial burst
crossChainOfferSummaries.add(historicOfferSummary);
// Save initial AT mode
previousAtModes.put(historicAtState.getATAddress(), historicOfferSummary.getMode());
}
}
}
} catch (DataException e) {
session.close(4003, "generic repository issue");
return;
}
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
session.close(4004, "websocket issue");
return;
}
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo, previousAtModes);
BlockNotifier.getInstance().register(session, listener);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
BlockNotifier.getInstance().deregister(session);
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, BlockInfo blockInfo, final Map<String, BTCACCT.Mode> previousAtModes) {
List<CrossChainOfferSummary> crossChainOfferSummaries = null;
try (final Repository repository = RepositoryManager.getRepository()) {
// Find any new trade ATs since this block
// Find any new/changed trade ATs since this block
final Boolean isFinished = null;
final Integer dataByteOffset = null;
final Long expectedValue = null;
final Integer minimumFinalHeight = blockInfo.getHeight();
final Integer minimumFinalHeight = blockData.getHeight();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
@@ -149,12 +91,13 @@ public class TradeOffersWebSocket extends ApiWebSocket {
if (atStates == null)
return;
crossChainOfferSummaries = produceSummaries(repository, atStates, blockInfo.getTimestamp());
crossChainOfferSummaries = produceSummaries(repository, atStates, blockData.getTimestamp());
} catch (DataException e) {
// No output this time
return;
}
synchronized (previousAtModes) { //NOSONAR squid:S2445 suppressed because previousAtModes is final and curried in lambda
synchronized (previousAtModes) {
// Remove any entries unchanged from last time
crossChainOfferSummaries.removeIf(offerSummary -> previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
@@ -162,13 +105,78 @@ public class TradeOffersWebSocket extends ApiWebSocket {
if (crossChainOfferSummaries.isEmpty())
return;
final boolean wasSent = sendOfferSummaries(session, crossChainOfferSummaries);
// Update
for (CrossChainOfferSummary offerSummary : crossChainOfferSummaries) {
previousAtModes.put(offerSummary.qortalAtAddress, offerSummary.getMode());
LOGGER.trace(() -> String.format("Block height: %d, AT: %s, mode: %s", blockData.getHeight(), offerSummary.qortalAtAddress, offerSummary.getMode().name()));
if (!wasSent)
return;
switch (offerSummary.getMode()) {
case OFFERING:
currentSummaries.put(offerSummary.qortalAtAddress, offerSummary);
historicSummaries.remove(offerSummary.qortalAtAddress);
break;
previousAtModes.putAll(crossChainOfferSummaries.stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, CrossChainOfferSummary::getMode)));
case REDEEMED:
case REFUNDED:
case CANCELLED:
currentSummaries.remove(offerSummary.qortalAtAddress);
historicSummaries.put(offerSummary.qortalAtAddress, offerSummary);
break;
case TRADING:
currentSummaries.remove(offerSummary.qortalAtAddress);
historicSummaries.remove(offerSummary.qortalAtAddress);
break;
}
}
// Remove any historic offers that are over 24 hours old
final long tooOldTimestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
historicSummaries.values().removeIf(historicSummary -> historicSummary.getTimestamp() < tooOldTimestamp);
}
// Notify sessions
for (Session session : getSessions())
sendOfferSummaries(session, crossChainOfferSummaries);
}
@OnWebSocketConnect
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
synchronized (previousAtModes) {
crossChainOfferSummaries.addAll(currentSummaries.values());
if (includeHistoric)
crossChainOfferSummaries.addAll(historicSummaries.values());
}
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
session.close(4002, "websocket issue");
return;
}
super.onWebSocketConnect(session);
}
@OnWebSocketClose
@Override
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
/* ignored */
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private boolean sendOfferSummaries(Session session, List<CrossChainOfferSummary> crossChainOfferSummaries) {
@@ -186,6 +194,61 @@ public class TradeOffersWebSocket extends ApiWebSocket {
return true;
}
private static void populateCurrentSummaries(Repository repository) throws DataException {
// We want ALL OFFERING trades
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET;
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value;
Integer minimumFinalHeight = null;
List<ATStateData> initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (initialAtStates == null)
throw new DataException("Couldn't fetch current trades from repository");
// Save initial AT modes
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
// Convert to offer summaries
currentSummaries.putAll(produceSummaries(repository, initialAtStates, null).stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary)));
}
private static void populateHistoricSummaries(Repository repository) throws DataException {
// We want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
long timestamp = System.currentTimeMillis() - 24 * 60 * 60 * 1000L;
int minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
if (minimumFinalHeight == 0)
throw new DataException("Couldn't fetch block timestamp from repository");
Boolean isFinished = Boolean.TRUE;
Integer dataByteOffset = null;
Long expectedValue = null;
++minimumFinalHeight; // because height is just *before* timestamp
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (historicAtStates == null)
throw new DataException("Couldn't fetch historic trades from repository");
for (ATStateData historicAtState : historicAtStates) {
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
if (!isHistoric.test(historicOfferSummary))
continue;
// Add summary to initial burst
historicSummaries.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary);
// Save initial AT mode
previousAtModes.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary.getMode());
}
}
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState);
@@ -193,7 +256,7 @@ public class TradeOffersWebSocket extends ApiWebSocket {
if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING)
// We want when trade was created, not when it was last updated
atStateTimestamp = atState.getCreation();
atStateTimestamp = crossChainTradeData.creationTimestamp;
else
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());

View File

@@ -51,16 +51,17 @@ public class AT {
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
byte[] codeHash = Crypto.digest(machineState.getCodeBytes());
byte[] codeBytes = machineState.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), codeHash,
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
machineState.isFrozen(), machineState.getFrozenBalance());
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, 0L, true);
this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true);
}
// Getters / setters
@@ -106,12 +107,11 @@ public class AT {
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
}
long creation = this.atData.getCreation();
byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData);
long atFees = api.calcFinalFees(state);
this.atStateData = new ATStateData(atAddress, blockHeight, creation, stateData, stateHash, atFees, false);
this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false);
return api.getTransactions();
}

View File

@@ -6,6 +6,8 @@ import static java.util.stream.Collectors.toMap;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -15,6 +17,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
@@ -29,6 +32,7 @@ import org.qortal.controller.Controller;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.EligibleQoraHolderData;
import org.qortal.data.account.QortFromQoraData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.at.ATData;
@@ -53,7 +57,6 @@ import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import org.roaringbitmap.IntIterator;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
@@ -128,7 +131,7 @@ public class Block {
@FunctionalInterface
private interface BlockRewardDistributor {
long distribute(long amount, Map<Account, Long> balanceChanges) throws DataException;
long distribute(long amount, Map<String, Long> balanceChanges) throws DataException;
}
/** Lazy-instantiated expanded info on block's online accounts. */
@@ -144,8 +147,8 @@ public class Block {
private final Account recipientAccount;
private final AccountData recipientAccountData;
ExpandedAccount(Repository repository, int accountIndex) throws DataException {
this.rewardShareData = repository.getAccountRepository().getRewardShareByIndex(accountIndex);
ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException {
this.rewardShareData = rewardShareData;
this.sharePercent = this.rewardShareData.getSharePercent();
this.mintingAccount = new Account(repository, this.rewardShareData.getMinter());
@@ -188,12 +191,12 @@ public class Block {
return shareBinsByLevel[accountLevel];
}
public long distribute(long accountAmount, Map<Account, Long> balanceChanges) {
public long distribute(long accountAmount, Map<String, Long> balanceChanges) {
if (this.isRecipientAlsoMinter) {
// minter & recipient the same - simpler case
LOGGER.trace(() -> String.format("Minter/recipient account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(accountAmount)));
if (accountAmount != 0)
balanceChanges.merge(this.mintingAccount, accountAmount, Long::sum);
balanceChanges.merge(this.mintingAccount.getAddress(), accountAmount, Long::sum);
} else {
// minter & recipient different - extra work needed
long recipientAmount = (accountAmount * this.sharePercent) / 100L / 100L; // because scaled by 2dp and 'percent' means "per 100"
@@ -201,11 +204,11 @@ public class Block {
LOGGER.trace(() -> String.format("Minter account %s share: %s", this.mintingAccount.getAddress(), Amounts.prettyAmount(minterAmount)));
if (minterAmount != 0)
balanceChanges.merge(this.mintingAccount, minterAmount, Long::sum);
balanceChanges.merge(this.mintingAccount.getAddress(), minterAmount, Long::sum);
LOGGER.trace(() -> String.format("Recipient account %s share: %s", this.recipientAccount.getAddress(), Amounts.prettyAmount(recipientAmount)));
if (recipientAmount != 0)
balanceChanges.merge(this.recipientAccount, recipientAmount, Long::sum);
balanceChanges.merge(this.recipientAccount.getAddress(), recipientAmount, Long::sum);
}
// We always distribute all of the amount
@@ -217,6 +220,8 @@ public class Block {
/** Opportunistic cache of this block's valid online accounts. Only created by call to isValid(). */
private List<OnlineAccountData> cachedValidOnlineAccounts = null;
/** Opportunistic cache of this block's valid online reward-shares. Only created by call to isValid(). */
private List<RewardShareData> cachedOnlineRewardShares = null;
// Other useful constants
@@ -567,22 +572,28 @@ public class Block {
/**
* Return expanded info on block's online accounts.
* <p>
* Typically called as part of Block.process() or Block.orphan()
* so ideally after any calls to Block.isValid().
*
* @throws DataException
*/
public List<ExpandedAccount> getExpandedAccounts() throws DataException {
if (this.cachedExpandedAccounts != null)
return this.cachedExpandedAccounts;
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
// We might already have a cache of online, reward-shares thanks to isValid()
if (this.cachedOnlineRewardShares == null) {
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
this.cachedOnlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(accountIndexes.toArray());
if (this.cachedOnlineRewardShares == null)
throw new DataException("Online accounts invalid?");
}
List<ExpandedAccount> expandedAccounts = new ArrayList<>();
IntIterator iterator = accountIndexes.iterator();
while (iterator.hasNext()) {
int accountIndex = iterator.next();
ExpandedAccount accountInfo = new ExpandedAccount(repository, accountIndex);
expandedAccounts.add(accountInfo);
}
for (RewardShareData rewardShare : this.cachedOnlineRewardShares)
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
this.cachedExpandedAccounts = expandedAccounts;
@@ -783,15 +794,46 @@ public class Block {
return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(ACCOUNTS_COUNT_SHIFT).add(keyDistance);
}
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries) {
public static BigInteger calcChainWeight(int commonBlockHeight, byte[] commonBlockSignature, List<BlockSummaryData> blockSummaries, int maxHeight) {
BigInteger cumulativeWeight = BigInteger.ZERO;
int parentHeight = commonBlockHeight;
byte[] parentBlockSignature = commonBlockSignature;
NumberFormat formatter = new DecimalFormat("0.###E0");
boolean isLogging = LOGGER.getLevel().isLessSpecificThan(Level.TRACE);
for (BlockSummaryData blockSummaryData : blockSummaries) {
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT).add(calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData));
StringBuilder stringBuilder = isLogging ? new StringBuilder(512) : null;
if (isLogging)
stringBuilder.append(formatter.format(cumulativeWeight)).append(" -> ");
cumulativeWeight = cumulativeWeight.shiftLeft(CHAIN_WEIGHT_SHIFT);
if (isLogging)
stringBuilder.append(formatter.format(cumulativeWeight)).append(" + ");
BigInteger blockWeight = calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
if (isLogging)
stringBuilder.append("(height: ")
.append(parentHeight + 1)
.append(", online: ")
.append(blockSummaryData.getOnlineAccountsCount())
.append(") ")
.append(formatter.format(blockWeight));
cumulativeWeight = cumulativeWeight.add(blockWeight);
if (isLogging)
stringBuilder.append(" -> ").append(formatter.format(cumulativeWeight));
if (isLogging && blockSummaries.size() > 1)
LOGGER.debug(() -> stringBuilder.toString()); //NOSONAR S1612 (false positive?)
parentHeight = blockSummaryData.getHeight();
parentBlockSignature = blockSummaryData.getSignature();
/* Potential future consensus change: only comparing the same number of blocks.
if (parentHeight >= maxHeight)
break;
*/
}
return cumulativeWeight;
@@ -917,19 +959,9 @@ public class Block {
if (accountIndexes.size() != this.blockData.getOnlineAccountsCount())
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
List<RewardShareData> expandedAccounts = new ArrayList<>();
IntIterator iterator = accountIndexes.iterator();
while (iterator.hasNext()) {
int accountIndex = iterator.next();
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShareByIndex(accountIndex);
// Check that claimed online account actually exists
if (rewardShareData == null)
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
expandedAccounts.add(rewardShareData);
}
List<RewardShareData> onlineRewardShares = repository.getAccountRepository().getRewardSharesByIndexes(accountIndexes.toArray());
if (onlineRewardShares == null)
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
// If block is past a certain age then we simply assume the signatures were correct
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
@@ -939,7 +971,7 @@ public class Block {
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
if (this.blockData.getOnlineAccountsSignatures().length != expandedAccounts.size() * Transformer.SIGNATURE_LENGTH)
if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
// Check signatures
@@ -961,7 +993,7 @@ public class Block {
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
byte[] signature = onlineAccountsSignatures.get(i);
byte[] publicKey = expandedAccounts.get(i).getRewardSharePublicKey();
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
ourOnlineAccounts.add(onlineAccountData);
@@ -982,6 +1014,7 @@ public class Block {
// All online accounts valid, so save our list of online accounts for potential later use
this.cachedValidOnlineAccounts = ourOnlineAccounts;
this.cachedOnlineRewardShares = onlineRewardShares;
return ValidationResult.OK;
}
@@ -1316,13 +1349,16 @@ public class Block {
allUniqueExpandedAccounts.add(expandedAccount.recipientAccountData);
}
// Decrease blocks minted count for all accounts
// Increase blocks minted count for all accounts
// Batch update in repository
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
// Local changes and also checks for level bump
for (AccountData accountData : allUniqueExpandedAccounts) {
// Adjust count locally (in Java)
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
int rowCount = repository.getAccountRepository().modifyMintedBlockCount(accountData.getAddress(), +1);
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s (rowCount: %d)", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""), rowCount));
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
@@ -1612,12 +1648,14 @@ public class Block {
}
// Decrease blocks minted count for all accounts
// Batch update in repository
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), -1);
for (AccountData accountData : allUniqueExpandedAccounts) {
// Adjust count locally (in Java)
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
int rowCount = repository.getAccountRepository().modifyMintedBlockCount(accountData.getAddress(), -1);
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s (rowCount: %d)", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : ""), rowCount));
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
@@ -1646,7 +1684,7 @@ public class Block {
this.distributionMethod = distributionMethod;
}
public long distribute(long distibutionAmount, Map<Account, Long> balanceChanges) throws DataException {
public long distribute(long distibutionAmount, Map<String, Long> balanceChanges) throws DataException {
return this.distributionMethod.distribute(distibutionAmount, balanceChanges);
}
}
@@ -1663,7 +1701,7 @@ public class Block {
// Now distribute to candidates
// Collate all balance changes and then apply in one final step
Map<Account, Long> balanceChanges = new HashMap<>();
Map<String, Long> balanceChanges = new HashMap<>();
long remainingAmount = totalAmount;
for (int r = 0; r < rewardCandidates.size(); ++r) {
@@ -1688,8 +1726,10 @@ public class Block {
}
// Apply balance changes
for (Map.Entry<Account, Long> balanceChange : balanceChanges.entrySet())
balanceChange.getKey().modifyAssetBalance(Asset.QORT, balanceChange.getValue());
List<AccountBalanceData> accountBalanceDeltas = balanceChanges.entrySet().stream()
.map(entry -> new AccountBalanceData(entry.getKey(), Asset.QORT, entry.getValue()))
.collect(Collectors.toList());
this.repository.getAccountRepository().modifyAssetBalances(accountBalanceDeltas);
}
protected List<BlockRewardCandidate> determineBlockRewardCandidates(boolean isProcessingNotOrphaning) throws DataException {
@@ -1759,7 +1799,7 @@ public class Block {
}
// Fetch list of legacy QORA holders who haven't reached their cap of QORT reward.
List<AccountBalanceData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
List<EligibleQoraHolderData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
final boolean haveQoraHolders = !qoraHolders.isEmpty();
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare();
@@ -1812,7 +1852,7 @@ public class Block {
return rewardCandidates;
}
private static long distributeBlockRewardShare(long distributionAmount, List<ExpandedAccount> accounts, Map<Account, Long> balanceChanges) {
private static long distributeBlockRewardShare(long distributionAmount, List<ExpandedAccount> accounts, Map<String, Long> balanceChanges) {
// Collate all expanded accounts by minting account
Map<String, List<ExpandedAccount>> accountsByMinter = new HashMap<>();
@@ -1841,7 +1881,7 @@ public class Block {
return sharedAmount;
}
private static long distributeBlockRewardToQoraHolders(long qoraHoldersAmount, List<AccountBalanceData> qoraHolders, Map<Account, Long> balanceChanges, Block block) throws DataException {
private static long distributeBlockRewardToQoraHolders(long qoraHoldersAmount, List<EligibleQoraHolderData> qoraHolders, Map<String, Long> balanceChanges, Block block) throws DataException {
final boolean isProcessingNotOrphaning = qoraHoldersAmount >= 0;
long qoraPerQortReward = BlockChain.getInstance().getQoraPerQortReward();
@@ -1849,7 +1889,7 @@ public class Block {
long totalQoraHeld = 0;
for (int i = 0; i < qoraHolders.size(); ++i)
totalQoraHeld += qoraHolders.get(i).getBalance();
totalQoraHeld += qoraHolders.get(i).getQoraBalance();
long finalTotalQoraHeld = totalQoraHeld;
LOGGER.trace(() -> String.format("Total legacy QORA held: %s", Amounts.prettyAmount(finalTotalQoraHeld)));
@@ -1862,9 +1902,13 @@ public class Block {
BigInteger totalQoraHeldBI = BigInteger.valueOf(totalQoraHeld);
long sharedAmount = 0;
// For batched update of QORT_FROM_QORA balances
List<AccountBalanceData> newQortFromQoraBalances = new ArrayList<>();
for (int h = 0; h < qoraHolders.size(); ++h) {
AccountBalanceData qoraHolder = qoraHolders.get(h);
BigInteger qoraHolderBalanceBI = BigInteger.valueOf(qoraHolder.getBalance());
EligibleQoraHolderData qoraHolder = qoraHolders.get(h);
BigInteger qoraHolderBalanceBI = BigInteger.valueOf(qoraHolder.getQoraBalance());
String qoraHolderAddress = qoraHolder.getAddress();
// This is where a 128bit integer library could help:
// long holderReward = (qoraHoldersAmount * qoraHolder.getBalance()) / totalQoraHeld;
@@ -1872,15 +1916,13 @@ public class Block {
final long holderRewardForLogging = holderReward;
LOGGER.trace(() -> String.format("QORA holder %s has %s / %s QORA so share: %s",
qoraHolder.getAddress(), Amounts.prettyAmount(qoraHolder.getBalance()), finalTotalQoraHeld, Amounts.prettyAmount(holderRewardForLogging)));
qoraHolderAddress, Amounts.prettyAmount(qoraHolder.getQoraBalance()), finalTotalQoraHeld, Amounts.prettyAmount(holderRewardForLogging)));
// Too small to register this time?
if (holderReward == 0)
continue;
Account qoraHolderAccount = new Account(block.repository, qoraHolder.getAddress());
long newQortFromQoraBalance = qoraHolderAccount.getConfirmedBalance(Asset.QORT_FROM_QORA) + holderReward;
long newQortFromQoraBalance = qoraHolder.getQortFromQoraBalance() + holderReward;
// If processing, make sure we don't overpay
if (isProcessingNotOrphaning) {
@@ -1894,44 +1936,43 @@ public class Block {
newQortFromQoraBalance -= adjustment;
// This is also the QORA holder's final QORT-from-QORA block
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolder.getAddress(), holderReward, block.blockData.getHeight());
QortFromQoraData qortFromQoraData = new QortFromQoraData(qoraHolderAddress, holderReward, block.blockData.getHeight());
block.repository.getAccountRepository().save(qortFromQoraData);
long finalAdjustedHolderReward = holderReward;
LOGGER.trace(() -> String.format("QORA holder %s final share %s at height %d",
qoraHolder.getAddress(), Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
qoraHolderAddress, Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
}
} else {
// Orphaning
QortFromQoraData qortFromQoraData = block.repository.getAccountRepository().getQortFromQoraInfo(qoraHolder.getAddress());
if (qortFromQoraData != null) {
if (qoraHolder.getFinalBlockHeight() != null) {
// Final QORT-from-QORA amount from repository was stored during processing, and hence positive.
// So we use + here as qortFromQora is negative during orphaning.
// More efficient than "holderReward - (0 - final-qort-from-qora)"
long adjustment = holderReward + qortFromQoraData.getFinalQortFromQora();
long adjustment = holderReward + qoraHolder.getFinalQortFromQora();
holderReward -= adjustment;
newQortFromQoraBalance -= adjustment;
block.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolder.getAddress());
block.repository.getAccountRepository().deleteQortFromQoraInfo(qoraHolderAddress);
long finalAdjustedHolderReward = holderReward;
LOGGER.trace(() -> String.format("QORA holder %s final share %s was at height %d",
qoraHolder.getAddress(), Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
qoraHolderAddress, Amounts.prettyAmount(finalAdjustedHolderReward), block.blockData.getHeight()));
}
}
balanceChanges.merge(qoraHolderAccount, holderReward, Long::sum);
balanceChanges.merge(qoraHolderAddress, holderReward, Long::sum);
if (newQortFromQoraBalance > 0)
qoraHolderAccount.setConfirmedBalance(Asset.QORT_FROM_QORA, newQortFromQoraBalance);
else
// Remove QORT_FROM_QORA balance as it's zero
qoraHolderAccount.deleteBalance(Asset.QORT_FROM_QORA);
// Add to batched QORT_FROM_QORA balance update list
newQortFromQoraBalances.add(new AccountBalanceData(qoraHolderAddress, Asset.QORT_FROM_QORA, newQortFromQoraBalance));
sharedAmount += holderReward;
}
// Perform batched update of QORT_FROM_QORA balances
block.repository.getAccountRepository().setAssetBalances(newQortFromQoraBalances);
return sharedAmount;
}

View File

@@ -32,7 +32,6 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.NTP;
import org.qortal.utils.StringLongMapXmlAdapter;
/**
@@ -532,7 +531,8 @@ public class BlockChain {
private static void rebuildBlockchain() throws DataException {
// (Re)build repository
RepositoryManager.rebuild();
if (!RepositoryManager.wasPristineAtOpen())
RepositoryManager.rebuild();
try (final Repository repository = RepositoryManager.getRepository()) {
GenesisBlock genesisBlock = GenesisBlock.getInstance(repository);
@@ -554,17 +554,23 @@ public class BlockChain {
try {
try (final Repository repository = RepositoryManager.getRepository()) {
for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) {
int height = repository.getBlockRepository().getBlockchainHeight();
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(height);
while (height > targetHeight) {
LOGGER.info(String.format("Forcably orphaning block %d", height));
BlockData blockData = repository.getBlockRepository().fromHeight(height);
Block block = new Block(repository, blockData);
Block block = new Block(repository, orphanBlockData);
block.orphan();
repository.saveChanges();
}
BlockData lastBlockData = repository.getBlockRepository().getLastBlock();
Controller.getInstance().setChainTip(lastBlockData);
repository.saveChanges();
--height;
orphanBlockData = repository.getBlockRepository().fromHeight(height);
repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onOrphanedBlock(orphanBlockData);
}
return true;
}
@@ -573,33 +579,4 @@ public class BlockChain {
}
}
public static void trimOldOnlineAccountsSignatures() {
final Long now = NTP.getTime();
if (now == null)
return;
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock())
// Too busy to trim right now, try again later
return;
try {
try (final Repository repository = RepositoryManager.tryRepository()) {
if (repository == null)
return;
int numBlocksTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(now - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime());
if (numBlocksTrimmed > 0)
LOGGER.debug(String.format("Trimmed old online accounts signatures from %d block%s", numBlocksTrimmed, (numBlocksTrimmed != 1 ? "s" : "")));
repository.saveChanges();
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue trying to trim old online accounts signatures: %s", e.getMessage()));
}
} finally {
blockchainLock.unlock();
}
}
}

View File

@@ -0,0 +1,77 @@
package org.qortal.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.NTP;
public class AtStatesTrimmer implements Runnable {
private static final Logger LOGGER = LogManager.getLogger(AtStatesTrimmer.class);
@Override
public void run() {
Thread.currentThread().setName("AT States trimmer");
try (final Repository repository = RepositoryManager.getRepository()) {
repository.getATRepository().prepareForAtStateTrimming();
repository.saveChanges();
while (!Controller.isStopping()) {
repository.discardChanges();
Thread.sleep(Settings.getInstance().getAtStatesTrimInterval());
BlockData chainTip = Controller.getInstance().getChainTip();
if (chainTip == null || NTP.getTime() == null)
continue;
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
if (Controller.getInstance().isSynchronizing())
continue;
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
// We want to keep AT states near the tip of our copy of blockchain so we can process/orphan nearby blocks
long chainTrimmableTimestamp = chainTip.getTimestamp() - Settings.getInstance().getAtStatesMaxLifetime();
long upperTrimmableTimestamp = Math.min(currentTrimmableTimestamp, chainTrimmableTimestamp);
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
int upperBatchHeight = trimStartHeight + Settings.getInstance().getAtStatesTrimBatchSize();
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
if (trimStartHeight >= upperTrimHeight)
continue;
int numAtStatesTrimmed = repository.getATRepository().trimAtStates(trimStartHeight, upperTrimHeight, Settings.getInstance().getAtStatesTrimLimit());
repository.saveChanges();
if (numAtStatesTrimmed > 0) {
LOGGER.debug(() -> String.format("Trimmed %d AT state%s between blocks %d and %d",
numAtStatesTrimmed, (numAtStatesTrimmed != 1 ? "s" : ""),
trimStartHeight, upperTrimHeight));
} else {
// Can we move onto next batch?
if (upperTrimmableHeight > upperBatchHeight) {
repository.getATRepository().setAtTrimHeight(upperBatchHeight);
repository.getATRepository().prepareForAtStateTrimming();
repository.saveChanges();
LOGGER.debug(() -> String.format("Bumping AT state trim height to %d", upperBatchHeight));
}
}
}
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue trying to trim AT states: %s", e.getMessage()));
} catch (InterruptedException e) {
// Time to exit
}
}
}

View File

@@ -1,4 +1,4 @@
package org.qortal.block;
package org.qortal.controller;
import java.math.BigInteger;
import java.util.ArrayList;
@@ -13,8 +13,9 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.block.Block;
import org.qortal.block.Block.ValidationResult;
import org.qortal.controller.Controller;
import org.qortal.block.BlockChain;
import org.qortal.data.account.MintingAccountData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.block.BlockData;
@@ -60,7 +61,7 @@ public class BlockMinter extends Thread {
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
for (TransactionData transactionData : unconfirmedTransactions) {
LOGGER.trace(String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
LOGGER.trace(() -> String.format("Deleting unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
repository.getTransactionRepository().delete(transactionData);
}
@@ -69,7 +70,7 @@ public class BlockMinter extends Thread {
// Going to need this a lot...
BlockRepository blockRepository = repository.getBlockRepository();
Block previousBlock = null;
BlockData previousBlockData = null;
List<Block> newBlocks = new ArrayList<>();
@@ -115,7 +116,7 @@ public class BlockMinter extends Thread {
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
if (rewardShareData == null) {
// Reward-share doesn't even exist - probably not a good sign
// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
madi.remove();
continue;
}
@@ -150,8 +151,8 @@ public class BlockMinter extends Thread {
isMintingPossible = true;
// Check blockchain hasn't changed
if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) {
previousBlock = new Block(repository, lastBlockData);
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
previousBlockData = lastBlockData;
newBlocks.clear();
// Reduce log timeout
@@ -162,12 +163,12 @@ public class BlockMinter extends Thread {
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
// Do we need to build any potential new blocks?
List<PrivateKeyAccount> mintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
for (PrivateKeyAccount mintingAccount : mintingAccounts) {
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
// First block does the AT heavy-lifting
if (newBlocks.isEmpty()) {
Block newBlock = Block.mint(repository, previousBlock.getBlockData(), mintingAccount);
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
if (newBlock == null) {
// For some reason we can't mint right now
moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
@@ -195,7 +196,7 @@ public class BlockMinter extends Thread {
// Make sure we're the only thread modifying the blockchain
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
LOGGER.warn("Couldn't acquire blockchain lock even after waiting 30 seconds");
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
continue;
}
@@ -233,8 +234,8 @@ public class BlockMinter extends Thread {
continue;
// Pick best block
final int parentHeight = previousBlock.getBlockData().getHeight();
final byte[] parentBlockSignature = previousBlock.getSignature();
final int parentHeight = previousBlockData.getHeight();
final byte[] parentBlockSignature = previousBlockData.getSignature();
BigInteger bestWeight = null;
@@ -274,9 +275,10 @@ public class BlockMinter extends Thread {
try {
newBlock.process();
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
repository.saveChanges();
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
if (rewardShareData != null) {
@@ -292,9 +294,7 @@ public class BlockMinter extends Thread {
newBlock.getMinter().getAddress()));
}
repository.saveChanges();
// Notify controller
// Notify controller after we're released blockchain lock
newBlockMinted = true;
} catch (DataException e) {
// Unable to process block - report and discard
@@ -305,8 +305,16 @@ public class BlockMinter extends Thread {
blockchainLock.unlock();
}
if (newBlockMinted)
Controller.getInstance().onNewBlock(newBlock.getBlockData());
if (newBlockMinted) {
// Notify Controller and broadcast our new chain to network
BlockData newBlockData = newBlock.getBlockData();
repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onNewBlock(newBlockData);
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
}
}
} catch (DataException e) {
LOGGER.warn("Repository issue while running block minter", e);

View File

@@ -1,61 +0,0 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.qortal.api.model.BlockInfo;
import org.qortal.data.block.BlockData;
public class BlockNotifier {
private static BlockNotifier instance;
@FunctionalInterface
public interface Listener {
void notify(BlockInfo blockInfo);
}
private Map<Session, Listener> listenersBySession = new HashMap<>();
private BlockNotifier() {
}
public static synchronized BlockNotifier getInstance() {
if (instance == null)
instance = new BlockNotifier();
return instance;
}
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public void onNewBlock(BlockData blockData) {
// Convert BlockData to BlockInfo
BlockInfo blockInfo = new BlockInfo(blockData);
for (Listener listener : getAllListeners())
listener.notify(blockInfo);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -16,16 +16,24 @@ import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -36,7 +44,6 @@ import org.qortal.account.PublicKeyAccount;
import org.qortal.api.ApiService;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockMinter;
import org.qortal.block.BlockChain.BlockTimingByHeight;
import org.qortal.controller.Synchronizer.SynchronizationResult;
import org.qortal.crypto.Crypto;
@@ -50,6 +57,8 @@ import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.ArbitraryTransactionData.DataType;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.globalization.Translator;
import org.qortal.gui.Gui;
@@ -85,6 +94,7 @@ import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.utils.Base58;
import org.qortal.utils.ByteArray;
import org.qortal.utils.DaemonThreadFactory;
import org.qortal.utils.NTP;
import org.qortal.utils.Triple;
@@ -132,16 +142,31 @@ public class Controller extends Thread {
private ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
private volatile boolean notifyGroupMembershipChange = false;
private volatile BlockData chainTip = null;
private static final int BLOCK_CACHE_SIZE = 10; // To cover typical Synchronizer request + a few spare
/** Latest blocks on our chain. Note: tail/last is the latest block. */
private final Deque<BlockData> latestBlocks = new LinkedList<>();
/** Cache of BlockMessages, indexed by block signature */
@SuppressWarnings("serial")
private final LinkedHashMap<ByteArray, BlockMessage> blockMessageCache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<ByteArray, BlockMessage> eldest) {
return this.size() > BLOCK_CACHE_SIZE;
}
};
private long repositoryBackupTimestamp = startTime; // ms
private long repositoryCheckpointTimestamp = startTime; // ms
private long ntpCheckTimestamp = startTime; // ms
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
private long onlineAccountsTasksTimestamp = startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
/** Whether we can mint new blocks, as reported by BlockMinter. */
private volatile boolean isMintingPossible = false;
/** Synchronization object for sync variables below */
private final Object syncLock = new Object();
/** Whether we are attempting to synchronize. */
private volatile boolean isSynchronizing = false;
/** Temporary estimate of synchronization progress for SysTray use. */
@@ -177,6 +202,47 @@ public class Controller extends Thread {
/** Cache of latest blocks' online accounts */
Deque<List<OnlineAccountData>> latestBlocksOnlineAccounts = new ArrayDeque<>(MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS);
// Stats
@XmlAccessorType(XmlAccessType.FIELD)
public static class StatsSnapshot {
public static class GetBlockMessageStats {
public AtomicLong requests = new AtomicLong();
public AtomicLong cacheHits = new AtomicLong();
public AtomicLong unknownBlocks = new AtomicLong();
public AtomicLong cacheFills = new AtomicLong();
public GetBlockMessageStats() {
}
}
public GetBlockMessageStats getBlockMessageStats = new GetBlockMessageStats();
public static class GetBlockSummariesStats {
public AtomicLong requests = new AtomicLong();
public AtomicLong cacheHits = new AtomicLong();
public AtomicLong fullyFromCache = new AtomicLong();
public GetBlockSummariesStats() {
}
}
public GetBlockSummariesStats getBlockSummariesStats = new GetBlockSummariesStats();
public static class GetBlockSignaturesV2Stats {
public AtomicLong requests = new AtomicLong();
public AtomicLong cacheHits = new AtomicLong();
public AtomicLong fullyFromCache = new AtomicLong();
public GetBlockSignaturesV2Stats() {
}
}
public GetBlockSignaturesV2Stats getBlockSignaturesV2Stats = new GetBlockSignaturesV2Stats();
public AtomicLong latestBlocksCacheRefills = new AtomicLong();
public StatsSnapshot() {
}
}
private final StatsSnapshot stats = new StatsSnapshot();
// Constructors
private Controller(String[] args) {
@@ -232,21 +298,36 @@ public class Controller extends Thread {
/** Returns current blockchain height, or 0 if it's not available. */
public int getChainHeight() {
BlockData blockData = this.chainTip;
if (blockData == null)
return 0;
synchronized (this.latestBlocks) {
BlockData blockData = this.latestBlocks.peekLast();
if (blockData == null)
return 0;
return blockData.getHeight();
return blockData.getHeight();
}
}
/** Returns highest block, or null if it's not available. */
public BlockData getChainTip() {
return this.chainTip;
synchronized (this.latestBlocks) {
return this.latestBlocks.peekLast();
}
}
/** Cache new blockchain tip. */
public void setChainTip(BlockData blockData) {
this.chainTip = blockData;
public void refillLatestBlocksCache() throws DataException {
// Set initial chain height/tip
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
synchronized (this.latestBlocks) {
this.latestBlocks.clear();
for (int i = 0; i < BLOCK_CACHE_SIZE && blockData != null; ++i) {
this.latestBlocks.addFirst(blockData);
blockData = repository.getBlockRepository().fromHeight(blockData.getHeight() - 1);
}
}
}
}
public ReentrantLock getBlockchainLock() {
@@ -271,7 +352,9 @@ public class Controller extends Thread {
}
public Integer getSyncPercent() {
return this.isSynchronizing ? this.syncPercent : null;
synchronized (this.syncLock) {
return this.isSynchronizing ? this.syncPercent : null;
}
}
// Entry point
@@ -326,13 +409,8 @@ public class Controller extends Thread {
try {
BlockChain.validate();
// Set initial chain height/tip
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
Controller.getInstance().setChainTip(blockData);
LOGGER.info(String.format("Our chain height at start-up: %d", blockData.getHeight()));
}
Controller.getInstance().refillLatestBlocksCache();
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
} catch (DataException e) {
LOGGER.error("Couldn't validate blockchain", e);
Gui.getInstance().fatalError("Blockchain validation issue", e);
@@ -366,6 +444,9 @@ public class Controller extends Thread {
blockMinter = new BlockMinter();
blockMinter.start();
LOGGER.info("Starting trade-bot");
TradeBot.getInstance();
// Arbitrary transaction data manager
// LOGGER.info("Starting arbitrary-transaction data manager");
// ArbitraryDataManager.getInstance().start();
@@ -404,6 +485,11 @@ public class Controller extends Thread {
Thread.currentThread().setName("Controller");
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
ExecutorService trimExecutor = Executors.newCachedThreadPool(new DaemonThreadFactory());
trimExecutor.execute(new AtStatesTrimmer());
trimExecutor.execute(new OnlineAccountsSignaturesTrimmer());
try {
while (!isStopping) {
@@ -445,6 +531,18 @@ public class Controller extends Thread {
final long requestMinimumTimestamp = now - ARBITRARY_REQUEST_TIMEOUT;
arbitraryDataRequests.entrySet().removeIf(entry -> entry.getValue().getC() < requestMinimumTimestamp);
// Time to 'checkpoint' uncommitted repository writes?
if (now >= repositoryCheckpointTimestamp + repositoryCheckpointInterval) {
repositoryCheckpointTimestamp = now + repositoryCheckpointInterval;
if (Settings.getInstance().getShowCheckpointNotification())
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "DB_CHECKPOINT"),
Translator.INSTANCE.translate("SysTray", "PERFORMING_DB_CHECKPOINT"),
MessageType.INFO);
RepositoryManager.checkpoint(true);
}
// Give repository a chance to backup (if enabled)
if (repositoryBackupInterval > 0 && now >= repositoryBackupTimestamp + repositoryBackupInterval) {
repositoryBackupTimestamp = now + repositoryBackupInterval;
@@ -477,7 +575,17 @@ public class Controller extends Thread {
}
}
} catch (InterruptedException e) {
// Clear interrupted flag so we can shutdown trim threads
Thread.interrupted();
// Fall-through to exit
} finally {
trimExecutor.shutdownNow();
try {
trimExecutor.awaitTermination(2L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// We tried...
}
}
}
@@ -510,6 +618,10 @@ public class Controller extends Thread {
};
private void potentiallySynchronize() throws InterruptedException {
// Already synchronizing via another thread?
if (this.isSynchronizing)
return;
List<Peer> peers = Network.getInstance().getHandshakedPeers();
// Disregard peers that have "misbehaved" recently
@@ -542,14 +654,21 @@ public class Controller extends Thread {
}
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
// Only update SysTray if we're potentially changing height
if (syncPercent < 100) {
isSynchronizing = true;
updateSysTray();
boolean hasStatusChanged = false;
BlockData priorChainTip = this.getChainTip();
synchronized (this.syncLock) {
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
// Only update SysTray if we're potentially changing height
if (this.syncPercent < 100) {
this.isSynchronizing = true;
hasStatusChanged = true;
}
}
BlockData priorChainTip = this.chainTip;
if (hasStatusChanged)
updateSysTray();
try {
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
@@ -619,8 +738,8 @@ public class Controller extends Thread {
// Reset our cache of inferior chains
inferiorChainSignatures.clear();
// Update chain-tip, systray, notify peers, websockets, etc.
this.onNewBlock(newChainTip);
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
}
return syncResult;
@@ -629,6 +748,11 @@ public class Controller extends Thread {
}
}
public static class StatusChangeEvent implements Event {
public StatusChangeEvent() {
}
}
private void updateSysTray() {
if (NTP.getTime() == null) {
SysTray.getInstance().setToolTipText(Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_CLOCK"));
@@ -643,20 +767,23 @@ public class Controller extends Thread {
String heightText = Translator.INSTANCE.translate("SysTray", "BLOCK_HEIGHT");
String actionText;
if (isMintingPossible)
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
else if (isSynchronizing)
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), syncPercent);
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers())
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
else
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
synchronized (this.syncLock) {
if (this.isMintingPossible)
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
else if (this.isSynchronizing)
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), this.syncPercent);
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers())
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
else
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
}
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height);
SysTray.getInstance().setToolTipText(tooltip);
this.callbackExecutor.execute(() -> {
StatusNotifier.getInstance().onStatusChange(NTP.getTime());
EventBus.INSTANCE.notify(new StatusChangeEvent());
});
}
@@ -783,25 +910,126 @@ public class Controller extends Thread {
requestSysTrayUpdate = true;
}
public static class NewBlockEvent implements Event {
private final BlockData blockData;
public NewBlockEvent(BlockData blockData) {
this.blockData = blockData;
}
public BlockData getBlockData() {
return this.blockData;
}
}
/**
* Callback for when we've received a new block.
* <p>
* See <b>WARNING</b> for {@link EventBus#notify(Event)}
* to prevent deadlocks.
*/
public void onNewBlock(BlockData latestBlockData) {
this.setChainTip(latestBlockData);
requestSysTrayUpdate = true;
// Protective copy
BlockData blockDataCopy = new BlockData(latestBlockData);
// Broadcast our new height info and notify websocket listeners
this.callbackExecutor.execute(() -> {
Network network = Network.getInstance();
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
synchronized (this.latestBlocks) {
BlockData cachedChainTip = this.latestBlocks.peekLast();
BlockNotifier.getInstance().onNewBlock(latestBlockData);
if (cachedChainTip != null && Arrays.equals(cachedChainTip.getSignature(), blockDataCopy.getReference())) {
// Chain tip is parent for new latest block, so we can safely add new latest block
this.latestBlocks.addLast(latestBlockData);
if (this.notifyGroupMembershipChange) {
this.notifyGroupMembershipChange = false;
ChatNotifier.getInstance().onGroupMembershipChange();
// Trim if necessary
if (this.latestBlocks.size() >= BLOCK_CACHE_SIZE)
this.latestBlocks.pollFirst();
} else {
if (cachedChainTip != null)
// Chain tip didn't match - potentially abnormal behaviour?
LOGGER.debug(() -> String.format("Cached chain tip %.8s not parent for new latest block %.8s (reference %.8s)",
Base58.encode(cachedChainTip.getSignature()),
Base58.encode(blockDataCopy.getSignature()),
Base58.encode(blockDataCopy.getReference())));
// Defensively rebuild cache
try {
this.stats.latestBlocksCacheRefills.incrementAndGet();
this.refillLatestBlocksCache();
} catch (DataException e) {
LOGGER.warn(() -> "Couldn't refill latest blocks cache?", e);
}
}
}
this.onNewOrOrphanedBlock(blockDataCopy, NewBlockEvent::new);
}
public static class OrphanedBlockEvent implements Event {
private final BlockData blockData;
public OrphanedBlockEvent(BlockData blockData) {
this.blockData = blockData;
}
public BlockData getBlockData() {
return this.blockData;
}
}
/**
* Callback for when we've orphaned a block.
* <p>
* See <b>WARNING</b> for {@link EventBus#notify(Event)}
* to prevent deadlocks.
*/
public void onOrphanedBlock(BlockData latestBlockData) {
// Protective copy
BlockData blockDataCopy = new BlockData(latestBlockData);
synchronized (this.latestBlocks) {
BlockData cachedChainTip = this.latestBlocks.pollLast();
boolean refillNeeded = false;
if (cachedChainTip != null && Arrays.equals(cachedChainTip.getReference(), blockDataCopy.getSignature())) {
// Chain tip was parent for new latest block that has been orphaned, so we're good
// However, if we've emptied the cache then we will need to refill it
refillNeeded = this.latestBlocks.isEmpty();
} else {
if (cachedChainTip != null)
// Chain tip didn't match - potentially abnormal behaviour?
LOGGER.debug(() -> String.format("Cached chain tip %.8s (reference %.8s) was not parent for new latest block %.8s",
Base58.encode(cachedChainTip.getSignature()),
Base58.encode(cachedChainTip.getReference()),
Base58.encode(blockDataCopy.getSignature())));
// Defensively rebuild cache
refillNeeded = true;
}
// Trade-bot might want to perform some actions too
TradeBot.getInstance().onChainTipChange();
});
if (refillNeeded)
try {
this.stats.latestBlocksCacheRefills.incrementAndGet();
this.refillLatestBlocksCache();
} catch (DataException e) {
LOGGER.warn(() -> "Couldn't refill latest blocks cache?", e);
}
}
this.onNewOrOrphanedBlock(blockDataCopy, OrphanedBlockEvent::new);
}
private void onNewOrOrphanedBlock(BlockData blockDataCopy, Function<BlockData, Event> eventConstructor) {
requestSysTrayUpdate = true;
// Notify listeners, trade-bot, etc.
EventBus.INSTANCE.notify(eventConstructor.apply(blockDataCopy));
if (this.notifyGroupMembershipChange) {
this.notifyGroupMembershipChange = false;
ChatNotifier.getInstance().onGroupMembershipChange();
}
}
/** Callback for when we've received a new transaction via API or peer. */
@@ -896,11 +1124,31 @@ public class Controller extends Thread {
private void onNetworkGetBlockMessage(Peer peer, Message message) {
GetBlockMessage getBlockMessage = (GetBlockMessage) message;
byte[] signature = getBlockMessage.getSignature();
this.stats.getBlockMessageStats.requests.incrementAndGet();
ByteArray signatureAsByteArray = new ByteArray(signature);
BlockMessage cachedBlockMessage = this.blockMessageCache.get(signatureAsByteArray);
// Check cached latest block message
if (cachedBlockMessage != null) {
this.stats.getBlockMessageStats.cacheHits.incrementAndGet();
// We need to duplicate it to prevent multiple threads setting ID on the same message
BlockMessage clonedBlockMessage = cachedBlockMessage.cloneWithNewId(message.getId());
if (!peer.sendMessage(clonedBlockMessage))
peer.disconnect("failed to send block");
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData == null) {
// We don't have this block
this.stats.getBlockMessageStats.unknownBlocks.getAndIncrement();
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
@@ -915,10 +1163,19 @@ public class Controller extends Thread {
Block block = new Block(repository, blockData);
Message blockMessage = new BlockMessage(block);
BlockMessage blockMessage = new BlockMessage(block);
blockMessage.setId(message.getId());
// This call also causes the other needed data to be pulled in from repository
if (!peer.sendMessage(blockMessage))
peer.disconnect("failed to send block");
// If request is for a recent block, cache it
if (getChainHeight() - blockData.getHeight() <= BLOCK_CACHE_SIZE) {
this.stats.getBlockMessageStats.cacheFills.incrementAndGet();
this.blockMessageCache.put(new ByteArray(blockData.getSignature()), blockMessage);
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send block %s to peer %s", Base58.encode(signature), peer), e);
}
@@ -965,59 +1222,110 @@ public class Controller extends Thread {
private void onNetworkGetBlockSummariesMessage(Peer peer, Message message) {
GetBlockSummariesMessage getBlockSummariesMessage = (GetBlockSummariesMessage) message;
byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
final byte[] parentSignature = getBlockSummariesMessage.getParentSignature();
this.stats.getBlockSummariesStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockSummaryData> blockSummaries = new ArrayList<>();
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
do {
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
if (blockData == null)
// No more blocks to send to peer
break;
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
blockSummaries.add(blockSummary);
parentSignature = blockData.getSignature();
} while (blockSummaries.size() < numberRequested);
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
// If peer's parent signature matches our latest block signature
// then we can short-circuit with an empty response
BlockData chainTip = getChainTip();
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
return;
}
List<BlockSummaryData> blockSummaries = new ArrayList<>();
// Attempt to serve from our cache of latest blocks
synchronized (this.latestBlocks) {
blockSummaries = this.latestBlocks.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.map(BlockSummaryData::new)
.collect(Collectors.toList());
}
if (blockSummaries.isEmpty()) {
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = Math.min(Network.MAX_BLOCK_SUMMARIES_PER_REPLY, getBlockSummariesMessage.getNumberRequested());
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
while (blockData != null && blockSummaries.size() < numberRequested) {
BlockSummaryData blockSummary = new BlockSummaryData(blockData);
blockSummaries.add(blockSummary);
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending block summaries after %s to peer %s", Base58.encode(parentSignature), peer), e);
}
} else {
this.stats.getBlockSummariesStats.cacheHits.incrementAndGet();
if (blockSummaries.size() >= getBlockSummariesMessage.getNumberRequested())
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
}
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
}
private void onNetworkGetSignaturesV2Message(Peer peer, Message message) {
GetSignaturesV2Message getSignaturesMessage = (GetSignaturesV2Message) message;
byte[] parentSignature = getSignaturesMessage.getParentSignature();
final byte[] parentSignature = getSignaturesMessage.getParentSignature();
this.stats.getBlockSignaturesV2Stats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = new ArrayList<>();
do {
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
if (blockData == null)
// No more signatures to send to peer
break;
parentSignature = blockData.getSignature();
signatures.add(parentSignature);
} while (signatures.size() < getSignaturesMessage.getNumberRequested());
Message signaturesMessage = new SignaturesMessage(signatures);
// If peer's parent signature matches our latest block signature
// then we can short-circuit with an empty response
BlockData chainTip = getChainTip();
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
Message signaturesMessage = new SignaturesMessage(Collections.emptyList());
signaturesMessage.setId(message.getId());
if (!peer.sendMessage(signaturesMessage))
peer.disconnect("failed to send signatures (v2)");
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
return;
}
List<byte[]> signatures = new ArrayList<>();
// Attempt to serve from our cache of latest blocks
synchronized (this.latestBlocks) {
signatures = this.latestBlocks.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.map(BlockData::getSignature)
.collect(Collectors.toList());
}
if (signatures.isEmpty()) {
try (final Repository repository = RepositoryManager.getRepository()) {
int numberRequested = getSignaturesMessage.getNumberRequested();
BlockData blockData = repository.getBlockRepository().fromReference(parentSignature);
while (blockData != null && signatures.size() < numberRequested) {
signatures.add(blockData.getSignature());
blockData = repository.getBlockRepository().fromReference(blockData.getSignature());
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending V2 signatures after %s to peer %s", Base58.encode(parentSignature), peer), e);
}
} else {
this.stats.getBlockSignaturesV2Stats.cacheHits.incrementAndGet();
if (signatures.size() >= getSignaturesMessage.getNumberRequested())
this.stats.getBlockSignaturesV2Stats.fullyFromCache.incrementAndGet();
}
Message signaturesMessage = new SignaturesMessage(signatures);
signaturesMessage.setId(message.getId());
if (!peer.sendMessage(signaturesMessage))
peer.disconnect("failed to send signatures (v2)");
}
private void onNetworkHeightV2Message(Peer peer, Message message) {
@@ -1367,9 +1675,6 @@ public class Controller extends Thread {
// Refresh our online accounts signatures?
sendOurOnlineAccountsInfo();
// Trim blockchain by removing 'old' online accounts signatures
BlockChain.trimOldOnlineAccountsSignatures();
}
private void sendOurOnlineAccountsInfo() {
@@ -1631,4 +1936,8 @@ public class Controller extends Thread {
return now - offset;
}
public StatsSnapshot getStatsSnapshot() {
return this.stats;
}
}

View File

@@ -0,0 +1,75 @@
package org.qortal.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.block.BlockChain;
import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.NTP;
public class OnlineAccountsSignaturesTrimmer implements Runnable {
private static final Logger LOGGER = LogManager.getLogger(OnlineAccountsSignaturesTrimmer.class);
private static final long INITIAL_SLEEP_PERIOD = 5 * 60 * 1000L + 1234L; // ms
public void run() {
Thread.currentThread().setName("Online Accounts trimmer");
try (final Repository repository = RepositoryManager.getRepository()) {
// Don't even start trimming until initial rush has ended
Thread.sleep(INITIAL_SLEEP_PERIOD);
while (!Controller.isStopping()) {
repository.discardChanges();
Thread.sleep(Settings.getInstance().getOnlineSignaturesTrimInterval());
BlockData chainTip = Controller.getInstance().getChainTip();
if (chainTip == null || NTP.getTime() == null)
continue;
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
if (Controller.getInstance().isSynchronizing())
continue;
// Trim blockchain by removing 'old' online accounts signatures
long upperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
int upperBatchHeight = trimStartHeight + Settings.getInstance().getOnlineSignaturesTrimBatchSize();
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
if (trimStartHeight >= upperTrimHeight)
continue;
int numSigsTrimmed = repository.getBlockRepository().trimOldOnlineAccountsSignatures(trimStartHeight, upperTrimHeight);
repository.saveChanges();
if (numSigsTrimmed > 0) {
LOGGER.debug(() -> String.format("Trimmed %d online accounts signature%s between blocks %d and %d",
numSigsTrimmed, (numSigsTrimmed != 1 ? "s" : ""),
trimStartHeight, upperTrimHeight));
} else {
// Can we move onto next batch?
if (upperTrimmableHeight > upperBatchHeight) {
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(upperBatchHeight);
repository.saveChanges();
LOGGER.debug(() -> String.format("Bumping online accounts signatures trim height to %d", upperBatchHeight));
}
}
}
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue trying to trim online accounts signatures: %s", e.getMessage()));
} catch (InterruptedException e) {
// Time to exit
}
}
}

View File

@@ -1,56 +0,0 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
public class StatusNotifier {
private static StatusNotifier instance;
@FunctionalInterface
public interface Listener {
void notify(long timestamp);
}
private Map<Session, Listener> listenersBySession = new HashMap<>();
private StatusNotifier() {
}
public static synchronized StatusNotifier getInstance() {
if (instance == null)
instance = new StatusNotifier();
return instance;
}
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public void onStatusChange(long now) {
for (Listener listener : getAllListeners())
listener.notify(now);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -4,6 +4,7 @@ import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@@ -11,11 +12,13 @@ import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.Block;
import org.qortal.block.Block.ValidationResult;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.transaction.RewardShareTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.Peer;
import org.qortal.network.message.BlockMessage;
@@ -172,7 +175,7 @@ public class Synchronizer {
* @throws DataException
* @throws InterruptedException
*/
private SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List<BlockSummaryData> blockSummariesFromCommon) throws DataException, InterruptedException {
public SynchronizationResult fetchSummariesFromCommonBlock(Repository repository, Peer peer, int ourHeight, boolean force, List<BlockSummaryData> blockSummariesFromCommon) throws DataException, InterruptedException {
// Start by asking for a few recent block hashes as this will cover a majority of reorgs
// Failing that, back off exponentially
int step = INITIAL_BLOCK_STEP;
@@ -238,15 +241,15 @@ public class Synchronizer {
blockSummariesFromCommon.addAll(blockSummariesBatch);
// Trim summaries so that first summary is common block.
// Currently we work back from the end until we hit a block we also have.
// Currently we work forward from common block until we hit a block we don't have
// TODO: rewrite as modified binary search!
for (int i = blockSummariesFromCommon.size() - 1; i > 0; --i) {
if (repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature())) {
// Note: index i isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
blockSummariesFromCommon.subList(0, i).clear();
int i;
for (i = 1; i < blockSummariesFromCommon.size(); ++i)
if (!repository.getBlockRepository().exists(blockSummariesFromCommon.get(i).getSignature()))
break;
}
}
// Note: index i - 1 isn't cleared: List.subList is fromIndex inclusive to toIndex exclusive
blockSummariesFromCommon.subList(0, i - 1).clear();
return SynchronizationResult.OK;
}
@@ -310,18 +313,21 @@ public class Synchronizer {
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository().getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
// Populate minter account levels for both lists of block summaries
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
populateBlockSummariesMinterLevels(repository, peerBlockSummaries);
final int mutualHeight = commonBlockHeight - 1 + Math.min(ourBlockSummaries.size(), peerBlockSummaries.size());
// Calculate cumulative chain weights of both blockchain subsets, from common block to highest mutual block.
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries);
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries);
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries, mutualHeight);
NumberFormat formatter = new DecimalFormat("0.###E0");
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
// If our blockchain has greater weight then don't synchronize with peer
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
LOGGER.debug(String.format("Not synchronizing with peer %s as we have better blockchain", peer));
NumberFormat formatter = new DecimalFormat("0.###E0");
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
return SynchronizationResult.INFERIOR_CHAIN;
}
}
@@ -394,15 +400,23 @@ public class Synchronizer {
// Unwind to common block (unless common block is our latest block)
LOGGER.debug(String.format("Orphaning blocks back to common block height %d, sig %.8s", commonBlockHeight, commonBlockSig58));
BlockData orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
while (ourHeight > commonBlockHeight) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
BlockData blockData = repository.getBlockRepository().fromHeight(ourHeight);
Block block = new Block(repository, blockData);
Block block = new Block(repository, orphanBlockData);
block.orphan();
LOGGER.trace(String.format("Orphaned block height %d, sig %.8s", ourHeight, Base58.encode(orphanBlockData.getSignature())));
repository.saveChanges();
--ourHeight;
orphanBlockData = repository.getBlockRepository().fromHeight(ourHeight);
repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onOrphanedBlock(orphanBlockData);
}
LOGGER.debug(String.format("Orphaned blocks back to height %d, sig %.8s - applying new blocks from peer %s", commonBlockHeight, commonBlockSig58, peer));
@@ -423,9 +437,11 @@ public class Synchronizer {
newBlock.process();
// If we've grown our blockchain then at least save progress so far
if (ourHeight > ourInitialHeight)
repository.saveChanges();
LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
repository.saveChanges();
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
return SynchronizationResult.OK;
@@ -505,9 +521,11 @@ public class Synchronizer {
newBlock.process();
// If we've grown our blockchain then at least save progress so far
if (ourHeight > ourInitialHeight)
repository.saveChanges();
LOGGER.trace(String.format("Processed block height %d, sig %.8s", newBlock.getBlockData().getHeight(), Base58.encode(newBlock.getBlockData().getSignature())));
repository.saveChanges();
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
return SynchronizationResult.OK;
@@ -550,16 +568,34 @@ public class Synchronizer {
}
private void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
final int firstBlockHeight = blockSummaries.get(0).getHeight();
for (int i = 0; i < blockSummaries.size(); ++i) {
BlockSummaryData blockSummary = blockSummaries.get(i);
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockSummary.getMinterPublicKey());
if (minterLevel == 0) {
// We don't want to throw, or use zero, as this will kill Controller thread and make client unstable.
// So we log this but use 1 instead
LOGGER.warn(String.format("Unexpected zero effective minter level for reward-share %s - using 1 instead!", Base58.encode(blockSummary.getMinterPublicKey())));
minterLevel = 1;
// It looks like this block's minter's reward-share has been cancelled.
// So search for REWARD_SHARE transactions since common block to find missing minter info
List<byte[]> transactionSignatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(Transaction.TransactionType.REWARD_SHARE, null, firstBlockHeight, null);
for (byte[] transactionSignature : transactionSignatures) {
RewardShareTransactionData transactionData = (RewardShareTransactionData) repository.getTransactionRepository().fromSignature(transactionSignature);
if (transactionData != null && Arrays.equals(transactionData.getRewardSharePublicKey(), blockSummary.getMinterPublicKey())) {
Account rewardShareMinter = new PublicKeyAccount(repository, transactionData.getMinterPublicKey());
minterLevel = rewardShareMinter.getEffectiveMintingLevel();
break;
}
}
if (minterLevel == 0) {
// We don't want to throw, or use zero, as this will kill Controller thread and make client unstable.
// So we log this but use 1 instead
LOGGER.debug(() -> String.format("Unexpected zero effective minter level for reward-share %s - using 1 instead!", Base58.encode(blockSummary.getMinterPublicKey())));
minterLevel = 1;
}
}
blockSummary.setMinterLevel(minterLevel);

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
@@ -20,6 +21,7 @@ import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.UTXO;
import org.bitcoinj.core.UTXOProvider;
import org.bitcoinj.core.UTXOProviderException;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicHierarchy;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.params.MainNetParams;
@@ -31,7 +33,6 @@ import org.bitcoinj.utils.MonetaryFormat;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.qortal.crosschain.ElectrumX.UnspentOutput;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.utils.BitTwiddling;
@@ -44,8 +45,17 @@ public class BTC {
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
public static final int HASH160_LENGTH = 20;
public static final boolean INCLUDE_UNCONFIRMED = true;
public static final boolean EXCLUDE_UNCONFIRMED = false;
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
// Temporary values until a dynamic fee system is written.
private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
private static final long NEW_FEE_AMOUNT = 10_000L;
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
@@ -75,6 +85,7 @@ public class BTC {
private static BTC instance;
private final NetworkParameters params;
private final ElectrumX electrumX;
private final Context bitcoinjContext;
// Let ECKey.equals() do the hard work
private final Set<ECKey> spentKeys = new HashSet<>();
@@ -88,6 +99,7 @@ public class BTC {
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name()));
this.electrumX = ElectrumX.getInstance(bitcoinNet.name());
this.bitcoinjContext = new Context(this.params);
}
public static synchronized BTC getInstance() {
@@ -119,6 +131,7 @@ public class BTC {
public boolean isValidXprv(String xprv58) {
try {
Context.propagate(bitcoinjContext);
DeterministicKey.deserializeB58(null, xprv58, this.params);
return true;
} catch (IllegalArgumentException e) {
@@ -128,25 +141,29 @@ public class BTC {
/** Returns P2PKH Bitcoin address using passed public key hash. */
public String pkhToAddress(byte[] publicKeyHash) {
Context.propagate(bitcoinjContext);
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
}
public String deriveP2shAddress(byte[] redeemScriptBytes) {
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Context.propagate(bitcoinjContext);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
return p2shAddress.toString();
}
/** Returns median timestamp from latest 11 blocks, in seconds. */
public Integer getMedianBlockTime() {
Integer height = this.electrumX.getCurrentHeight();
if (height == null)
return null;
/**
* Returns median timestamp from latest 11 blocks, in seconds.
* <p>
* @throws BitcoinException if error occurs
*/
public Integer getMedianBlockTime() throws BitcoinException {
int height = this.electrumX.getCurrentHeight();
// Grab latest 11 blocks
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height - 11, 11);
if (blockHeaders == null || blockHeaders.size() < 11)
return null;
if (blockHeaders.size() < 11)
throw new BitcoinException("Not enough blocks to determine median block time");
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
@@ -157,20 +174,45 @@ public class BTC {
return blockTimestamps.get(5);
}
public Long getBalance(String base58Address) {
return this.electrumX.getBalance(addressToScript(base58Address));
/**
* Returns estimated BTC fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch, or null for 'now'
* @return sats per 1000bytes, or throws BitcoinException if something went wrong
*/
public long estimateFee(Long timestamp) throws BitcoinException {
if (!this.params.getId().equals(NetworkParameters.ID_MAINNET))
return NON_MAINNET_FEE;
// TODO: This will need to be replaced with something better in the near future!
if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP)
return OLD_FEE_AMOUNT;
return NEW_FEE_AMOUNT;
}
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
if (unspentOutputs == null)
return null;
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if script unknown
* @throws BitcoinException if there was an error
*/
public long getConfirmedBalance(String base58Address) throws BitcoinException {
return this.electrumX.getConfirmedBalance(addressToScript(base58Address));
}
/**
* Returns list of unspent outputs pertaining to passed address.
* <p>
* @return list of unspent outputs, or empty list if address unknown
* @throws BitcoinException if there was an error.
*/
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws BitcoinException {
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address), false);
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
return null;
List<TransactionOutput> transactionOutputs = this.getOutputs(unspentOutput.hash);
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
}
@@ -178,22 +220,65 @@ public class BTC {
return unspentTransactionOutputs;
}
public List<TransactionOutput> getOutputs(byte[] txHash) {
/**
* Returns list of outputs pertaining to passed transaction hash.
* <p>
* @return list of outputs, or empty list if transaction unknown
* @throws BitcoinException if there was an error.
*/
public List<TransactionOutput> getOutputs(byte[] txHash) throws BitcoinException {
byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash);
if (rawTransactionBytes == null)
return null;
// XXX bitcoinj: replace with getTransaction() below
Context.propagate(bitcoinjContext);
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
return transaction.getOutputs();
}
/** Returns list of raw transactions spending passed address. */
public List<byte[]> getAddressTransactions(String base58Address) {
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
/**
* Returns list of transaction hashes pertaining to passed address.
* <p>
* @return list of unspent outputs, or empty list if script unknown
* @throws BitcoinException if there was an error.
*/
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws BitcoinException {
return this.electrumX.getAddressTransactions(addressToScript(base58Address), includeUnconfirmed);
}
public boolean broadcastTransaction(Transaction transaction) {
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
/**
* Returns list of raw, confirmed transactions involving given address.
* <p>
* @throws BitcoinException if there was an error
*/
public List<byte[]> getAddressTransactions(String base58Address) throws BitcoinException {
List<TransactionHash> transactionHashes = this.electrumX.getAddressTransactions(addressToScript(base58Address), false);
List<byte[]> rawTransactions = new ArrayList<>();
for (TransactionHash transactionInfo : transactionHashes) {
byte[] rawTransaction = this.electrumX.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
rawTransactions.add(rawTransaction);
}
return rawTransactions;
}
/**
* Returns transaction info for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction unknown
* @throws BitcoinException if error occurs
*/
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException {
return this.electrumX.getTransaction(txHash);
}
/**
* Broadcasts raw transaction to Bitcoin network.
* <p>
* @throws BitcoinException if error occurs
*/
public void broadcastTransaction(Transaction transaction) throws BitcoinException {
this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
}
/**
@@ -205,8 +290,9 @@ public class BTC {
* @return transaction, or null if insufficient funds
*/
public Transaction buildSpend(String xprv58, String recipient, long amount) {
Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ALL_SPENT));
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
Address destination = Address.fromString(this.params, recipient);
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
@@ -230,6 +316,7 @@ public class BTC {
* @return unspent BTC balance, or null if unable to determine balance
*/
public Long getWalletBalance(String xprv58) {
Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
@@ -240,6 +327,87 @@ public class BTC {
return balance.value;
}
/**
* Returns first unused receive address given 'm' BIP32 key.
*
* @param xprv58 BIP32 extended Bitcoin private key
* @return Bitcoin P2PKH address
* @throws BitcoinException if something went wrong
*/
public String getUnusedReceiveAddress(String xprv58) throws BitcoinException {
Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
keyChain.setLookaheadSize(WalletAwareUTXOProvider.LOOKAHEAD_INCREMENT);
keyChain.maybeLookAhead();
final int keyChainPathSize = keyChain.getAccountPath().size();
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
int ki = 0;
do {
for (; ki < keys.size(); ++ki) {
DeterministicKey dKey = keys.get(ki);
List<ChildNumber> dKeyPath = dKey.getPath();
// If keyChain is based on 'm', then make sure dKey is m/0/ki
if (dKeyPath.size() != keyChainPathSize + 2 || dKeyPath.get(dKeyPath.size() - 2) != ChildNumber.ZERO)
continue;
// Check unspent
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(script, false);
/*
* If there are no unspent outputs then either:
* a) all the outputs have been spent
* b) address has never been used
*
* For case (a) we want to remember not to check this address (key) again.
*/
if (unspentOutputs.isEmpty()) {
// If this is a known key that has been spent before, then we can skip asking for transaction history
if (this.spentKeys.contains(dKey)) {
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) dKey);
continue;
}
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.electrumX.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
this.spentKeys.add(dKey);
wallet.getActiveKeyChain().markKeyAsUsed(dKey);
} else {
// Key never been used - case (b)
return address.toString();
}
}
// Key has unspent outputs, hence used, so no good to us
this.spentKeys.remove(dKey);
}
// Generate some more keys
keyChain.setLookaheadSize(keyChain.getLookaheadSize() + WalletAwareUTXOProvider.LOOKAHEAD_INCREMENT);
keyChain.maybeLookAhead();
// This returns all keys, including those already in 'keys'
List<DeterministicKey> allLeafKeys = keyChain.getLeafKeys();
// Add only new keys onto our list of keys to search
List<DeterministicKey> newKeys = allLeafKeys.subList(ki, allLeafKeys.size());
keys.addAll(newKeys);
// Fall-through to checking more keys as now 'ki' is smaller than 'keys.size()' again
// Process new keys
} while (true);
}
// UTXOProvider support
static class WalletAwareUTXOProvider implements UTXOProvider {
@@ -280,9 +448,12 @@ public class BTC {
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
if (unspentOutputs == null)
List<UnspentOutput> unspentOutputs;
try {
unspentOutputs = btc.electrumX.getUnspentOutputs(script, false);
} catch (BitcoinException e) {
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
}
/*
* If there are no unspent outputs then either:
@@ -301,10 +472,12 @@ public class BTC {
}
// Ask for transaction history - if it's empty then key has never been used
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
if (historicTransactionHashes == null)
throw new UTXOProviderException(
String.format("Unable to fetch transaction history for %s", address));
List<TransactionHash> historicTransactionHashes;
try {
historicTransactionHashes = btc.electrumX.getAddressTransactions(script, false);
} catch (BitcoinException e) {
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
}
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
@@ -320,13 +493,17 @@ public class BTC {
}
// If we reach here, then there's definitely at least one unspent key
btc.spentKeys.remove(key);
areAllKeysSpent = false;
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
List<TransactionOutput> transactionOutputs;
try {
transactionOutputs = btc.getOutputs(unspentOutput.hash);
} catch (BitcoinException e) {
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
HashCode.fromBytes(unspentOutput.hash)));
}
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
@@ -359,11 +536,11 @@ public class BTC {
}
public int getChainHeadHeight() throws UTXOProviderException {
Integer height = btc.electrumX.getCurrentHeight();
if (height == null)
try {
return btc.electrumX.getCurrentHeight();
} catch (BitcoinException e) {
throw new UTXOProviderException("Unable to determine Bitcoin chain height");
return height.intValue();
}
}
public NetworkParameters getParams() {
@@ -374,6 +551,7 @@ public class BTC {
// Utility methods for us
private byte[] addressToScript(String base58Address) {
Context.propagate(bitcoinjContext);
Address address = Address.fromString(this.params, base58Address);
return ScriptBuilder.createOutputScript(address).getProgram();
}

View File

@@ -611,7 +611,7 @@ public class BTCACCT {
*/
public static CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
@@ -622,8 +622,8 @@ public class BTCACCT {
* @throws DataException
*/
public static CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
return populateTradeData(repository, creatorPublicKey, atStateData);
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
@@ -633,7 +633,7 @@ public class BTCACCT {
* @param atAddress
* @throws DataException
*/
public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
byte[] addressBytes = new byte[25]; // for general use
String atAddress = atStateData.getATAddress();
@@ -641,7 +641,7 @@ public class BTCACCT {
tradeData.qortalAtAddress = atAddress;
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
tradeData.creationTimestamp = atStateData.getCreation();
tradeData.creationTimestamp = creationTimestamp;
Account atAccount = new Account(repository, atAddress);
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
@@ -874,7 +874,8 @@ public class BTCACCT {
String atAddress = crossChainTradeData.qortalAtAddress;
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(atAddress, null, null, null);
// We don't have partner's public key so we check every message to AT
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
if (messageTransactionsData == null)
return null;

View File

@@ -1,9 +1,15 @@
package org.qortal.crosschain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Base58;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
@@ -25,6 +31,10 @@ import com.google.common.primitives.Bytes;
public class BTCP2SH {
public enum Status {
UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED
}
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
@@ -131,9 +141,10 @@ public class BTCP2SH {
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
* @param receivingAccountInfo Bitcoin PKH used for output
* @return Signed Bitcoin transaction for refunding P2SH
*/
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime, byte[] receivingAccountInfo) {
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
@@ -152,7 +163,7 @@ public class BTCP2SH {
};
// Send funds back to funding address
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, refundKey.getPubKeyHash());
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, receivingAccountInfo);
}
/**
@@ -233,4 +244,129 @@ public class BTCP2SH {
return null;
}
/** Returns P2SH status, given P2SH address and expected redeem/refund amount, or throws BitcoinException if error occurs. */
public static Status determineP2shStatus(String p2shAddress, long minimumAmount) throws BitcoinException {
final BTC btc = BTC.getInstance();
List<TransactionHash> transactionHashes = btc.getAddressTransactions(p2shAddress, BTC.INCLUDE_UNCONFIRMED);
// Sort by confirmed first, followed by ascending height
transactionHashes.sort(TransactionHash.CONFIRMED_FIRST.thenComparing(TransactionHash::getHeight));
// Transaction cache
Map<String, BitcoinTransaction> transactionsByHash = new HashMap<>();
// HASH160(redeem script) for this p2shAddress
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
// Check for spends first, caching full transaction info as we progress just in case we don't return in this loop
for (TransactionHash transactionInfo : transactionHashes) {
BitcoinTransaction bitcoinTransaction = btc.getTransaction(transactionInfo.txHash);
// Cache for possible later reuse
transactionsByHash.put(transactionInfo.txHash, bitcoinTransaction);
// Acceptable funding is one transaction output, so we're expecting only one input
if (bitcoinTransaction.inputs.size() != 1)
// Wrong number of inputs
continue;
String scriptSig = bitcoinTransaction.inputs.get(0).scriptSig;
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
// Not spending one of these P2SH
continue;
// Last chunk is redeem script
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
// Not spending our specific P2SH
continue;
// If we have 4 chunks, then secret is present
return scriptSigChunks.size() == 4
? (transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED)
: (transactionInfo.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED);
}
String ourScriptPubKey = HashCode.fromBytes(addressToScriptPubKey(p2shAddress)).toString();
// Check for funding
for (TransactionHash transactionInfo : transactionHashes) {
BitcoinTransaction bitcoinTransaction = transactionsByHash.get(transactionInfo.txHash);
if (bitcoinTransaction == null)
// Should be present in map!
throw new BitcoinException("Cached Bitcoin transaction now missing?");
// Check outputs for our specific P2SH
for (BitcoinTransaction.Output output : bitcoinTransaction.outputs) {
// Check amount
if (output.value < minimumAmount)
// Output amount too small (not taking fees into account)
continue;
String scriptPubKey = output.scriptPubKey;
if (!scriptPubKey.equals(ourScriptPubKey))
// Not funding our specific P2SH
continue;
return transactionInfo.height == 0 ? Status.FUNDING_IN_PROGRESS : Status.FUNDED;
}
}
return Status.UNFUNDED;
}
private static List<byte[]> extractScriptSigChunks(byte[] scriptSigBytes) {
List<byte[]> chunks = new ArrayList<>();
int offset = 0;
int previousOffset = 0;
while (offset < scriptSigBytes.length) {
byte pushOp = scriptSigBytes[offset++];
if (pushOp < 0 || pushOp > 0x4c)
// Unacceptable OP
return Collections.emptyList();
// Special treatment for OP_PUSHDATA1
if (pushOp == 0x4c) {
if (offset >= scriptSigBytes.length)
// Run out of scriptSig bytes?
return Collections.emptyList();
pushOp = scriptSigBytes[offset++];
}
previousOffset = offset;
offset += Byte.toUnsignedInt(pushOp);
byte[] chunk = Arrays.copyOfRange(scriptSigBytes, previousOffset, offset);
chunks.add(chunk);
}
return chunks;
}
private static byte[] addressToScriptPubKey(String p2shAddress) {
// We want the HASH160 part of the P2SH address
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
byte[] scriptPubKey = new byte[1 + 1 + 20 + 1];
scriptPubKey[0x00] = (byte) 0xa9; /* OP_HASH160 */
scriptPubKey[0x01] = (byte) 0x14; /* PUSH 0x14 bytes */
System.arraycopy(p2shAddressBytes, 1, scriptPubKey, 2, 0x14);
scriptPubKey[0x16] = (byte) 0x87; /* OP_EQUAL */
return scriptPubKey;
}
private static byte[] addressToRedeemScriptHash(String p2shAddress) {
// We want the HASH160 part of the P2SH address
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
return Arrays.copyOfRange(p2shAddressBytes, 1, 1 + 20);
}
}

View File

@@ -0,0 +1,57 @@
package org.qortal.crosschain;
@SuppressWarnings("serial")
public class BitcoinException extends Exception {
public BitcoinException() {
super();
}
public BitcoinException(String message) {
super(message);
}
public static class NetworkException extends BitcoinException {
private final Integer daemonErrorCode;
public NetworkException() {
super();
this.daemonErrorCode = null;
}
public NetworkException(String message) {
super(message);
this.daemonErrorCode = null;
}
public NetworkException(int errorCode, String message) {
super(message);
this.daemonErrorCode = errorCode;
}
public Integer getDaemonErrorCode() {
return this.daemonErrorCode;
}
}
public static class NotFoundException extends BitcoinException {
public NotFoundException() {
super();
}
public NotFoundException(String message) {
super(message);
}
}
public static class InsufficientFundsException extends BitcoinException {
public InsufficientFundsException() {
super();
}
public InsufficientFundsException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,31 @@
package org.qortal.crosschain;
import java.util.List;
interface BitcoinNetworkProvider {
/** Returns current blockchain height. */
int getCurrentHeight() throws BitcoinException;
/** Returns a list of raw block headers, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
List<byte[]> getRawBlockHeaders(int startHeight, int count) throws BitcoinException;
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
long getConfirmedBalance(byte[] scriptPubKey) throws BitcoinException;
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
byte[] getRawTransaction(String txHash) throws BitcoinException;
/** Returns unpacked transaction given <tt>txHash</tt>. */
BitcoinTransaction getTransaction(String txHash) throws BitcoinException;
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException;
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws BitcoinException;
/** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */
boolean broadcastTransaction(byte[] rawTransaction) throws BitcoinException;
}

View File

@@ -0,0 +1,70 @@
package org.qortal.crosschain;
import java.util.List;
import java.util.stream.Collectors;
public class BitcoinTransaction {
public final String txHash;
public final int size;
public final int locktime;
// Not present if transaction is unconfirmed
public final Integer timestamp;
public static class Input {
public final String scriptSig;
public final int sequence;
public final String outputTxHash;
public final int outputVout;
public Input(String scriptSig, int sequence, String outputTxHash, int outputVout) {
this.scriptSig = scriptSig;
this.sequence = sequence;
this.outputTxHash = outputTxHash;
this.outputVout = outputVout;
}
public String toString() {
return String.format("{output %s:%d, sequence %d, scriptSig %s}",
this.outputTxHash, this.outputVout, this.sequence, this.scriptSig);
}
}
public final List<Input> inputs;
public static class Output {
public final String scriptPubKey;
public final long value;
public Output(String scriptPubKey, long value) {
this.scriptPubKey = scriptPubKey;
this.value = value;
}
public String toString() {
return String.format("{value %d, scriptPubKey %s}", this.value, this.scriptPubKey);
}
}
public final List<Output> outputs;
public BitcoinTransaction(String txHash, int size, int locktime, Integer timestamp,
List<Input> inputs, List<Output> outputs) {
this.txHash = txHash;
this.size = size;
this.locktime = locktime;
this.timestamp = timestamp;
this.inputs = inputs;
this.outputs = outputs;
}
public String toString() {
return String.format("txHash %s, size %d, locktime %d, timestamp %d\n"
+ "\tinputs: [%s]\n"
+ "\toutputs: [%s]\n",
this.txHash,
this.size,
this.locktime,
this.timestamp,
this.inputs.stream().map(Input::toString).collect(Collectors.joining(",\n\t\t")),
this.outputs.stream().map(Output::toString).collect(Collectors.joining(",\n\t\t")));
}
}

View File

@@ -14,8 +14,9 @@ import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.apache.logging.log4j.LogManager;
@@ -35,17 +36,27 @@ public class ElectrumX {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
private static final Random RANDOM = new Random();
private static final double MIN_PROTOCOL_VERSION = 1.2;
private static final int DEFAULT_TCP_PORT = 50001;
private static final int DEFAULT_SSL_PORT = 50002;
private static final int BLOCK_HEADER_LENGTH = 80;
private static final String MAIN_GENESIS_HASH = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
private static final String TEST3_GENESIS_HASH = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
// We won't know REGTEST (i.e. local) genesis block hash
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
private static final Pattern DAEMON_ERROR_REGEX = Pattern.compile("DaemonError\\(\\{.*'code': ?(-?[0-9]+).*\\}\\)\\z"); // Capture 'code' inside curly-brace content
// Key: Bitcoin network (e.g. "MAIN", "TEST3", "REGTEST"), value: ElectrumX instance
private static final Map<String, ElectrumX> instances = new HashMap<>();
static class Server {
private static class Server {
String hostname;
enum ConnectionType { TCP, SSL };
enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
@@ -82,7 +93,9 @@ public class ElectrumX {
}
}
private Set<Server> servers = new HashSet<>();
private List<Server> remainingServers = new ArrayList<>();
private String expectedGenesisHash;
private Server currentServer;
private Socket socket;
private Scanner scanner;
@@ -93,34 +106,59 @@ public class ElectrumX {
private ElectrumX(String bitcoinNetwork) {
switch (bitcoinNetwork) {
case "MAIN":
servers.addAll(Arrays.asList(
this.expectedGenesisHash = MAIN_GENESIS_HASH;
this.servers.addAll(Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("tardis.bauerj.eu", Server.ConnectionType.SSL, 50002),
new Server("rbx.curalle.ovh", Server.ConnectionType.SSL, 50002),
new Server("quick.electumx.live", Server.ConnectionType.SSL, 50002),
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
new Server("electrumx.ddns.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
new Server("electrum.eff.ro", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512),
new Server("E-X.not.fyi", Server.ConnectionType.SSL, 50002),
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 50001),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
new Server("electrumx.electricnewyear.net", Server.ConnectionType.SSL, 50002),
new Server("dxm.no-ip.biz", Server.ConnectionType.TCP, 50001),
new Server("kirsche.emzy.de", Server.ConnectionType.TCP, 50001),
new Server("2AZZARITA.hopto.org", Server.ConnectionType.TCP, 50001),
new Server("xtrum.com", Server.ConnectionType.TCP, 50001),
new Server("electrum.srvmin.network", Server.ConnectionType.TCP, 50001),
new Server("electrumx.alexridevski.net", Server.ConnectionType.TCP, 50001),
new Server("bitcoin.lukechilds.co", Server.ConnectionType.TCP, 50001),
new Server("electrum.poiuty.com", Server.ConnectionType.TCP, 50001),
new Server("horsey.cryptocowboys.net", Server.ConnectionType.TCP, 50001),
new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001),
new Server("electrum-server.ninja", Server.ConnectionType.TCP, 50081),
new Server("bitcoin.electrumx.multicoin.co", Server.ConnectionType.TCP, 50001),
new Server("esx.geekhosters.com", Server.ConnectionType.TCP, 50001),
new Server("bitcoin.grey.pw", Server.ConnectionType.TCP, 50003),
new Server("exs.ignorelist.com", Server.ConnectionType.TCP, 50001),
new Server("electrum.coinext.com.br", Server.ConnectionType.TCP, 50001),
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001),
new Server("skbxmit.coinjoined.com", Server.ConnectionType.TCP, 50001),
new Server("alviss.coinjoined.com", Server.ConnectionType.TCP, 50001),
new Server("electrum2.privateservers.network", Server.ConnectionType.TCP, 50001),
new Server("electrumx.schulzemic.net", Server.ConnectionType.TCP, 50001),
new Server("bitcoins.sk", Server.ConnectionType.TCP, 56001),
new Server("node.mendonca.xyz", Server.ConnectionType.TCP, 50001),
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)));
break;
case "TEST3":
servers.addAll(Arrays.asList(
new Server("tn.not.fyi", Server.ConnectionType.TCP, 55001),
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
this.expectedGenesisHash = TEST3_GENESIS_HASH;
this.servers.addAll(Arrays.asList(
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 60001),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 60002),
new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
new Server("testnet.qtornado.com", Server.ConnectionType.TCP, 51001),
new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)));
break;
case "REGTEST":
servers.addAll(Arrays.asList(
this.expectedGenesisHash = null;
this.servers.addAll(Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, DEFAULT_TCP_PORT),
new Server("localhost", Server.ConnectionType.SSL, DEFAULT_SSL_PORT)));
break;
@@ -130,7 +168,6 @@ public class ElectrumX {
}
LOGGER.debug(() -> String.format("Starting ElectrumX support for %s Bitcoin network", bitcoinNetwork));
rpc("server.banner");
}
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
@@ -143,35 +180,50 @@ public class ElectrumX {
// Methods for use by other classes
public Integer getCurrentHeight() {
/**
* Returns current blockchain height.
* <p>
* @throws BitcoinException if error occurs
*/
public int getCurrentHeight() throws BitcoinException {
Object blockObj = this.rpc("blockchain.headers.subscribe");
if (!(blockObj instanceof JSONObject))
return null;
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.headers.subscribe RPC");
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("height"))
return null;
Object heightObj = blockJson.get("height");
return ((Long) blockJson.get("height")).intValue();
if (!(heightObj instanceof Long))
throw new BitcoinException.NetworkException("Missing/invalid 'height' in JSON from ElectrumX blockchain.headers.subscribe RPC");
return ((Long) heightObj).intValue();
}
public List<byte[]> getBlockHeaders(int startHeight, long count) {
/**
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws BitcoinException if error occurs
*/
public List<byte[]> getBlockHeaders(int startHeight, long count) throws BitcoinException {
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
if (!(blockObj instanceof JSONObject))
return null;
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.block.headers RPC");
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
return null;
Object countObj = blockJson.get("count");
Object hexObj = blockJson.get("hex");
Long returnedCount = (Long) blockJson.get("count");
String hex = (String) blockJson.get("hex");
if (!(countObj instanceof Long) || !(hexObj instanceof String))
throw new BitcoinException.NetworkException("Missing/invalid 'count' or 'hex' entries in JSON from ElectrumX blockchain.block.headers RPC");
Long returnedCount = (Long) countObj;
String hex = (String) hexObj;
byte[] raw = HashCode.fromString(hex).asBytes();
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
return null;
throw new BitcoinException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
for (int i = 0; i < returnedCount; ++i)
@@ -180,46 +232,43 @@ public class ElectrumX {
return rawBlockHeaders;
}
/** Returns confirmed balance, based on passed payment script, or null if there was an error or no known balance. */
public Long getBalance(byte[] script) {
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if script unknown
* @throws BitcoinException if there was an error
*/
public long getConfirmedBalance(byte[] script) throws BitcoinException {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (!(balanceObj instanceof JSONObject))
return null;
throw new BitcoinException.NetworkException("Unexpected output from ElectrumX blockchain.scripthash.get_balance RPC");
JSONObject balanceJson = (JSONObject) balanceObj;
if (!balanceJson.containsKey("confirmed"))
return null;
Object confirmedBalanceObj = balanceJson.get("confirmed");
if (!(confirmedBalanceObj instanceof Long))
throw new BitcoinException.NetworkException("Missing confirmed balance from ElectrumX blockchain.scripthash.get_balance RPC");
return (Long) balanceJson.get("confirmed");
}
/** Unspent output info as returned by ElectrumX network. */
public static class UnspentOutput {
public final byte[] hash;
public final int index;
public final int height;
public final long value;
public UnspentOutput(byte[] hash, int index, int height, long value) {
this.hash = hash;
this.index = index;
this.height = height;
this.value = value;
}
}
/** Returns list of unspent outputs pertaining to passed payment script, or null if there was an error. */
public List<UnspentOutput> getUnspentOutputs(byte[] script) {
/**
* Returns list of unspent outputs pertaining to passed payment script.
* <p>
* @return list of unspent outputs, or empty list if script unknown
* @throws BitcoinException if there was an error.
*/
public List<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws BitcoinException {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (!(unspentJson instanceof JSONArray))
return null;
throw new BitcoinException("Expected array output from ElectrumX blockchain.scripthash.listunspent RPC");
List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : (JSONArray) unspentJson) {
@@ -227,7 +276,7 @@ public class ElectrumX {
int height = ((Long) unspent.get("height")).intValue();
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
if (height <= 0)
if (!includeUnconfirmed && height <= 0)
continue;
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
@@ -240,68 +289,163 @@ public class ElectrumX {
return unspentOutputs;
}
/** Returns raw transaction for passed transaction hash, or null if not found. */
public byte[] getRawTransaction(byte[] txHash) {
Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
/**
* Returns raw transaction for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction not found
* @throws BitcoinException if error occurs
*/
public byte[] getRawTransaction(byte[] txHash) throws BitcoinException {
Object rawTransactionHex;
try {
rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
} catch (BitcoinException.NetworkException e) {
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
throw new BitcoinException.NotFoundException(e.getMessage());
throw e;
}
if (!(rawTransactionHex instanceof String))
return null;
throw new BitcoinException.NetworkException("Expected hex string as raw transaction from ElectrumX blockchain.transaction.get RPC");
return HashCode.fromString((String) rawTransactionHex).asBytes();
}
/** Returns list of raw transactions, relating to passed payment script, if null if there's an error. */
public List<byte[]> getAddressTransactions(byte[] script) {
/**
* Returns transaction info for passed transaction hash.
* <p>
* @throws BitcoinException.NotFoundException if transaction not found
* @throws BitcoinException if error occurs
*/
public BitcoinTransaction getTransaction(String txHash) throws BitcoinException {
Object transactionObj;
try {
transactionObj = this.rpc("blockchain.transaction.get", txHash, true);
} catch (BitcoinException.NetworkException e) {
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
throw new BitcoinException.NotFoundException(e.getMessage());
throw e;
}
if (!(transactionObj instanceof JSONObject))
throw new BitcoinException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC");
JSONObject transactionJson = (JSONObject) transactionObj;
Object inputsObj = transactionJson.get("vin");
if (!(inputsObj instanceof JSONArray))
throw new BitcoinException.NetworkException("Expected JSONArray for 'vin' from ElectrumX blockchain.transaction.get RPC");
Object outputsObj = transactionJson.get("vout");
if (!(outputsObj instanceof JSONArray))
throw new BitcoinException.NetworkException("Expected JSONArray for 'vout' from ElectrumX blockchain.transaction.get RPC");
try {
int size = ((Long) transactionJson.get("size")).intValue();
int locktime = ((Long) transactionJson.get("locktime")).intValue();
// Timestamp might not be present, e.g. for unconfirmed transaction
Object timeObj = transactionJson.get("time");
Integer timestamp = timeObj != null
? ((Long) timeObj).intValue()
: null;
List<BitcoinTransaction.Input> inputs = new ArrayList<>();
for (Object inputObj : (JSONArray) inputsObj) {
JSONObject inputJson = (JSONObject) inputObj;
String scriptSig = (String) ((JSONObject) inputJson.get("scriptSig")).get("hex");
int sequence = ((Long) inputJson.get("sequence")).intValue();
String outputTxHash = (String) inputJson.get("txid");
int outputVout = ((Long) inputJson.get("vout")).intValue();
inputs.add(new BitcoinTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
}
List<BitcoinTransaction.Output> outputs = new ArrayList<>();
for (Object outputObj : (JSONArray) outputsObj) {
JSONObject outputJson = (JSONObject) outputObj;
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
long value = (long) (((Double) outputJson.get("value")) * 1e8);
outputs.add(new BitcoinTransaction.Output(scriptPubKey, value));
}
return new BitcoinTransaction(txHash, size, locktime, timestamp, inputs, outputs);
} catch (NullPointerException | ClassCastException e) {
// Unexpected / invalid response from ElectrumX server
}
throw new BitcoinException.NetworkException("Unexpected JSON format from ElectrumX blockchain.transaction.get RPC");
}
/**
* Returns list of transactions, relating to passed payment script.
* <p>
* @return list of related transactions, or empty list if script unknown
* @throws BitcoinException if error occurs
*/
public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws BitcoinException {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (!(transactionsJson instanceof JSONArray))
return null;
throw new BitcoinException.NetworkException("Expected array output from ElectrumX blockchain.scripthash.get_history RPC");
List<byte[]> rawTransactions = new ArrayList<>();
List<TransactionHash> transactionHashes = new ArrayList<>();
for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
// We only want confirmed transactions
if (!transactionInfo.containsKey("height"))
Long height = (Long) transactionInfo.get("height");
if (!includeUnconfirmed && (height == null || height == 0))
// We only want confirmed transactions
continue;
String txHash = (String) transactionInfo.get("tx_hash");
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", txHash);
if (rawTransactionHex == null)
return null;
rawTransactions.add(HashCode.fromString(rawTransactionHex).asBytes());
transactionHashes.add(new TransactionHash(height.intValue(), txHash));
}
return rawTransactions;
return transactionHashes;
}
/** Returns true if raw transaction successfully broadcast. */
public boolean broadcastTransaction(byte[] transactionBytes) {
/**
* Broadcasts raw transaction to Bitcoin network.
* <p>
* @throws BitcoinException if error occurs
*/
public void broadcastTransaction(byte[] transactionBytes) throws BitcoinException {
Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
if (rawBroadcastResult == null)
return false;
// If result is a String, then it is simply transaction hash.
// Otherwise result is JSON and probably contains error info instead.
return rawBroadcastResult instanceof String;
// We're expecting a simple string that is the transaction hash
if (!(rawBroadcastResult instanceof String))
throw new BitcoinException.NetworkException("Unexpected response from ElectrumX blockchain.transaction.broadcast RPC");
}
// Class-private utility methods
/** Query current server for its list of peer servers, and return those we can parse. */
private Set<Server> serverPeersSubscribe() {
/**
* Query current server for its list of peer servers, and return those we can parse.
* <p>
* @throws BitcoinException
* @throws ClassCastException to be handled by caller
*/
private Set<Server> serverPeersSubscribe() throws BitcoinException {
Set<Server> newServers = new HashSet<>();
Object peers = this.connectedRpc("server.peers.subscribe");
if (!(peers instanceof JSONArray))
return newServers;
for (Object rawPeer : (JSONArray) peers) {
JSONArray peer = (JSONArray) rawPeer;
if (peer.size() < 3)
// We're expecting at least 3 fields for each peer entry: IP, hostname, features
continue;
String hostname = (String) peer.get(1);
@@ -322,9 +466,14 @@ public class ElectrumX {
connectionType = Server.ConnectionType.TCP;
port = DEFAULT_TCP_PORT;
break;
default:
// e.g. could be 'v' for protocol version, or 'p' for pruning limit
break;
}
if (connectionType == null)
// We couldn't extract any peer connection info?
continue;
// Possible non-default port?
@@ -344,8 +493,16 @@ public class ElectrumX {
return newServers;
}
/** Return output from RPC call, with automatic reconnection to different server if needed. */
private synchronized Object rpc(String method, Object...params) {
/**
* Performs RPC call, with automatic reconnection to different server if needed.
* <p>
* @return "result" object from within JSON output
* @throws BitcoinException if server returns error or something goes wrong
*/
private synchronized Object rpc(String method, Object...params) throws BitcoinException {
if (this.remainingServers.isEmpty())
this.remainingServers.addAll(this.servers);
while (haveConnection()) {
Object response = connectedRpc(method, params);
if (response != null)
@@ -360,18 +517,17 @@ public class ElectrumX {
this.scanner = null;
}
return null;
// Failed to perform RPC - maybe lack of servers?
throw new BitcoinException.NetworkException("Failed to perform Bitcoin RPC");
}
/** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() {
private boolean haveConnection() throws BitcoinException {
if (this.currentServer != null)
return true;
List<Server> remainingServers = new ArrayList<>(this.servers);
while (!remainingServers.isEmpty()) {
Server server = remainingServers.remove(RANDOM.nextInt(remainingServers.size()));
while (!this.remainingServers.isEmpty()) {
Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
@@ -384,23 +540,41 @@ public class ElectrumX {
if (server.connectionType == Server.ConnectionType.SSL) {
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
this.socket = (SSLSocket) factory.createSocket(this.socket, server.hostname, server.port, true);
this.socket = factory.createSocket(this.socket, server.hostname, server.port, true);
}
this.scanner = new Scanner(this.socket.getInputStream());
this.scanner.useDelimiter("\n");
// Check connection works by asking for more servers
// Check connection is suitable by asking for server features, including genesis block hash
JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features");
if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
continue;
if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
continue;
// Ask for more servers
Set<Server> moreServers = serverPeersSubscribe();
// Discard duplicate servers we already know
moreServers.removeAll(this.servers);
remainingServers.addAll(moreServers);
// Add to both lists
this.remainingServers.addAll(moreServers);
this.servers.addAll(moreServers);
LOGGER.debug(() -> String.format("Connected to %s", server));
this.currentServer = server;
return true;
} catch (IOException e) {
} catch (IOException | BitcoinException | ClassCastException | NullPointerException e) {
// Try another server...
if (this.socket != null && !this.socket.isClosed())
try {
this.socket.close();
} catch (IOException e1) {
// We did try...
}
this.socket = null;
this.scanner = null;
}
@@ -409,11 +583,20 @@ public class ElectrumX {
return false;
}
/**
* Perform RPC using currently connected server.
* <p>
* @param method
* @param params
* @return response Object, or null if server fails to respond
* @throws BitcoinException if server returns error
*/
@SuppressWarnings("unchecked")
private Object connectedRpc(String method, Object...params) {
private Object connectedRpc(String method, Object...params) throws BitcoinException {
JSONObject requestJson = new JSONObject();
requestJson.put("id", this.nextId++);
requestJson.put("method", method);
requestJson.put("jsonrpc", "2.0");
JSONArray requestParams = new JSONArray();
requestParams.addAll(Arrays.asList(params));
@@ -428,20 +611,52 @@ public class ElectrumX {
this.socket.getOutputStream().write(request.getBytes());
response = scanner.next();
} catch (IOException | NoSuchElementException e) {
// Unable to send, or receive -- try another server?
return null;
}
LOGGER.trace(() -> String.format("Response: %s", response));
if (response.isEmpty())
// Empty response - try another server?
return null;
Object responseObj = JSONValue.parse(response);
if (!(responseObj instanceof JSONObject))
// Unexpected response - try another server?
return null;
JSONObject responseJson = (JSONObject) responseObj;
Object errorObj = responseJson.get("error");
if (errorObj != null) {
if (!(errorObj instanceof JSONObject))
throw new BitcoinException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method));
JSONObject errorJson = (JSONObject) errorObj;
Object messageObj = errorJson.get("message");
if (!(messageObj instanceof String))
throw new BitcoinException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method));
String message = (String) messageObj;
// Some error 'messages' are actually wrapped upstream bitcoind errors:
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
// We want to detect these and extract the upstream error code for caller's use
Matcher messageMatcher = DAEMON_ERROR_REGEX.matcher(message);
if (messageMatcher.find())
try {
int daemonErrorCode = Integer.parseInt(messageMatcher.group(1));
throw new BitcoinException.NetworkException(daemonErrorCode, message);
} catch (NumberFormatException e) {
// We couldn't parse the error code integer? Fall-through to generic exception...
}
throw new BitcoinException.NetworkException(message);
}
return responseJson.get("result");
}

View File

@@ -0,0 +1,31 @@
package org.qortal.crosschain;
import java.util.Comparator;
public class TransactionHash {
public static final Comparator<TransactionHash> CONFIRMED_FIRST = (a, b) -> Boolean.compare(a.height != 0, b.height != 0);
public final int height;
public final String txHash;
public TransactionHash(int height, String txHash) {
this.height = height;
this.txHash = txHash;
}
public int getHeight() {
return this.height;
}
public String getTxHash() {
return this.txHash;
}
public String toString() {
return this.height == 0
? String.format("txHash %s (unconfirmed)", this.txHash)
: String.format("txHash %s (height %d)", this.txHash, this.height);
}
}

View File

@@ -0,0 +1,16 @@
package org.qortal.crosschain;
/** Unspent output info as returned by ElectrumX network. */
public class UnspentOutput {
public final byte[] hash;
public final int index;
public final int height;
public final long value;
public UnspentOutput(byte[] hash, int index, int height, long value) {
this.hash = hash;
this.index = index;
this.height = height;
this.value = value;
}
}

View File

@@ -1,5 +1,6 @@
package org.qortal.crypto;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@@ -42,6 +43,27 @@ public abstract class Crypto {
}
}
/**
* Returns 32-byte SHA-256 digest of message passed in input.
*
* @param input
* variable-length byte[] message
* @return byte[32] digest, or null if SHA-256 algorithm can't be accessed
*/
public static byte[] digest(ByteBuffer input) {
if (input == null)
return null;
try {
// SHA2-256
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(input);
return sha256.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 message digest not available");
}
}
/**
* Returns 32-byte digest of two rounds of SHA-256 on message passed in input.
*

View File

@@ -29,6 +29,10 @@ public class MemoryPoW {
do {
++nonce;
// If we've been interrupted, exit fast with invalid value
if (Thread.currentThread().isInterrupted())
return -1;
seed *= seedMultiplier; // per nonce
state[0] = longHash[0] ^ seed;

View File

@@ -0,0 +1,48 @@
package org.qortal.data.account;
public class EligibleQoraHolderData {
// Properties
private String address;
private long qoraBalance;
private long qortFromQoraBalance;
private Long finalQortFromQora;
private Integer finalBlockHeight;
// Constructors
public EligibleQoraHolderData(String address, long qoraBalance, long qortFromQoraBalance, Long finalQortFromQora,
Integer finalBlockHeight) {
this.address = address;
this.qoraBalance = qoraBalance;
this.qortFromQoraBalance = qortFromQoraBalance;
this.finalQortFromQora = finalQortFromQora;
this.finalBlockHeight = finalBlockHeight;
}
// Getters/Setters
public String getAddress() {
return this.address;
}
public long getQoraBalance() {
return this.qoraBalance;
}
public long getQortFromQoraBalance() {
return this.qortFromQoraBalance;
}
public Long getFinalQortFromQora() {
return this.finalQortFromQora;
}
public Integer getFinalBlockHeight() {
return this.finalBlockHeight;
}
}

View File

@@ -5,7 +5,6 @@ public class ATStateData {
// Properties
private String ATAddress;
private Integer height;
private Long creation;
private byte[] stateData;
private byte[] stateHash;
private Long fees;
@@ -14,10 +13,9 @@ public class ATStateData {
// Constructors
/** Create new ATStateData */
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
this.ATAddress = ATAddress;
this.height = height;
this.creation = creation;
this.stateData = stateData;
this.stateHash = stateHash;
this.fees = fees;
@@ -26,21 +24,21 @@ public class ATStateData {
/** For recreating per-block ATStateData from repository where not all info is needed */
public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
this(ATAddress, height, null, null, stateHash, fees, isInitial);
this(ATAddress, height, null, stateHash, fees, isInitial);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash) {
// This won't ever be initial AT state from deployment as that's never serialized over the network,
// but generated when the DeployAtTransaction is processed locally.
this(ATAddress, null, null, null, stateHash, null, false);
this(ATAddress, null, null, stateHash, null, false);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash, Long fees) {
// This won't ever be initial AT state from deployment as that's never serialized over the network,
// but generated when the DeployAtTransaction is processed locally.
this(ATAddress, null, null, null, stateHash, fees, false);
this(ATAddress, null, null, stateHash, fees, false);
}
// Getters / setters
@@ -58,10 +56,6 @@ public class ATStateData {
this.height = height;
}
public Long getCreation() {
return this.creation;
}
public byte[] getStateData() {
return this.stateData;
}

View File

@@ -79,6 +79,25 @@ public class BlockData implements Serializable {
null, 0, null, null);
}
public BlockData(BlockData other) {
this.version = other.version;
this.reference = other.reference;
this.transactionCount = other.transactionCount;
this.totalFees = other.totalFees;
this.transactionsSignature = other.transactionsSignature;
this.height = other.height;
this.timestamp = other.timestamp;
this.minterPublicKey = other.minterPublicKey;
this.minterSignature = other.minterSignature;
this.atCount = other.atCount;
this.atFees = other.atFees;
this.encodedOnlineAccounts = other.encodedOnlineAccounts;
this.onlineAccountsCount = other.onlineAccountsCount;
this.onlineAccountsTimestamp = other.onlineAccountsTimestamp;
this.onlineAccountsSignatures = other.onlineAccountsSignatures;
this.signature = other.signature;
}
// Getters/setters
public byte[] getSignature() {

View File

@@ -190,4 +190,9 @@ public class TradeBotData {
return this.receivingAccountInfo;
}
// Mostly for debugging
public String toString() {
return String.format("%s: %s", this.atAddress, this.tradeState.name());
}
}

View File

@@ -20,6 +20,21 @@ public enum EventBus {
}
}
/**
* <b>WARNING:</b> before calling this method,
* make sure repository holds no locks, e.g. by calling
* <tt>repository.discardChanges()</tt>.
* <p>
* This is because event listeners might open a new
* repository session which will deadlock HSQLDB
* if it tries to CHECKPOINT.
* <p>
* The HSQLDB deadlock occurs because the caller's
* repository session blocks the CHECKPOINT until
* their transaction is closed, yet event listeners
* new sessions are blocked until CHECKPOINT is
* completed, hence deadlock.
*/
public void notify(Event event) {
List<Listener> clonedListeners;

View File

@@ -10,12 +10,12 @@ import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.settings.Settings;
public enum Translator {
INSTANCE;
private static final Logger LOGGER = LogManager.getLogger(Translator.class);
private static final String DEFAULT_LANG = Locale.getDefault().getLanguage();
private static final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
@@ -34,7 +34,7 @@ public enum Translator {
}
public String translate(String className, String key) {
return this.translate(className, DEFAULT_LANG, key);
return this.translate(className, Settings.getInstance().getLocaleLang(), key);
}
public Set<String> keySet(String className, String lang) {

View File

@@ -96,22 +96,24 @@ public class Network {
private final String ourNodeId = Crypto.toNodeAddress(edPublicKeyParams.getEncoded());
private final int maxMessageSize;
private final int minOutboundPeers;
private final int maxPeers;
private final List<PeerData> allKnownPeers = new ArrayList<>();
private final List<Peer> connectedPeers = new ArrayList<>();
private final List<PeerAddress> selfPeers = new ArrayList<>();
private ExecuteProduceConsume networkEPC;
private final ExecuteProduceConsume networkEPC;
private Selector channelSelector;
private ServerSocketChannel serverChannel;
private Iterator<SelectionKey> channelIterator = null;
private int minOutboundPeers;
private int maxPeers;
private long nextConnectTaskTimestamp = 0L; // ms - try first connect once NTP syncs
// volatile because value is updated inside any one of the EPC threads
private volatile long nextConnectTaskTimestamp = 0L; // ms - try first connect once NTP syncs
private ExecutorService broadcastExecutor = Executors.newCachedThreadPool();
private long nextBroadcastTimestamp = 0L; // ms - try first broadcast once NTP syncs
// volatile because value is updated inside any one of the EPC threads
private volatile long nextBroadcastTimestamp = 0L; // ms - try first broadcast once NTP syncs
private final Lock mergePeersLock = new ReentrantLock();
@@ -429,35 +431,38 @@ public class Network {
private Task maybeProduceChannelTask(boolean canBlock) throws InterruptedException {
final SelectionKey nextSelectionKey;
// anything to do?
if (channelIterator == null) {
try {
if (canBlock)
channelSelector.select(1000L);
else
channelSelector.selectNow();
} catch (IOException e) {
LOGGER.warn(String.format("Channel selection threw IOException: %s", e.getMessage()));
return null;
// Synchronization here to enforce thread-safety on channelIterator
synchronized (channelSelector) {
// anything to do?
if (channelIterator == null) {
try {
if (canBlock)
channelSelector.select(1000L);
else
channelSelector.selectNow();
} catch (IOException e) {
LOGGER.warn(String.format("Channel selection threw IOException: %s", e.getMessage()));
return null;
}
if (Thread.currentThread().isInterrupted())
throw new InterruptedException();
channelIterator = channelSelector.selectedKeys().iterator();
}
if (Thread.currentThread().isInterrupted())
throw new InterruptedException();
if (channelIterator.hasNext()) {
nextSelectionKey = channelIterator.next();
channelIterator.remove();
} else {
nextSelectionKey = null;
channelIterator = null; // Nothing to do so reset iterator to cause new select
}
channelIterator = channelSelector.selectedKeys().iterator();
LOGGER.trace(() -> String.format("Thread %d, nextSelectionKey %s, channelIterator now %s",
Thread.currentThread().getId(), nextSelectionKey, channelIterator));
}
if (channelIterator.hasNext()) {
nextSelectionKey = channelIterator.next();
channelIterator.remove();
} else {
nextSelectionKey = null;
channelIterator = null; // Nothing to do so reset iterator to cause new select
}
LOGGER.trace(() -> String.format("Thread %d, nextSelectionKey %s, channelIterator now %s",
Thread.currentThread().getId(), nextSelectionKey, channelIterator));
if (nextSelectionKey == null)
return null;

View File

@@ -33,6 +33,7 @@ import org.qortal.settings.Settings;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.NTP;
import com.google.common.hash.HashCode;
import com.google.common.net.HostAndPort;
import com.google.common.net.InetAddresses;
@@ -348,21 +349,37 @@ public class Peer {
if (this.byteBuffer == null)
this.byteBuffer = ByteBuffer.allocate(Network.getInstance().getMaxMessageSize());
final int priorPosition = this.byteBuffer.position();
final int bytesRead = this.socketChannel.read(this.byteBuffer);
if (bytesRead == -1) {
this.disconnect("EOF");
return;
}
LOGGER.trace(() -> String.format("Received %d bytes from peer %s", bytesRead, this));
LOGGER.trace(() -> {
if (bytesRead > 0) {
byte[] leadingBytes = new byte[Math.min(bytesRead, 8)];
this.byteBuffer.asReadOnlyBuffer().position(priorPosition).get(leadingBytes);
String leadingHex = HashCode.fromBytes(leadingBytes).toString();
return String.format("Received %d bytes, starting %s, into byteBuffer[%d] from peer %s",
bytesRead,
leadingHex,
priorPosition,
this);
} else {
return String.format("Received %d bytes into byteBuffer[%d] from peer %s", bytesRead, priorPosition, this);
}
});
final boolean wasByteBufferFull = !this.byteBuffer.hasRemaining();
while (true) {
final Message message;
// Can we build a message from buffer now?
ByteBuffer readOnlyBuffer = this.byteBuffer.asReadOnlyBuffer().flip();
try {
message = Message.fromByteBuffer(this.byteBuffer);
message = Message.fromByteBuffer(readOnlyBuffer);
} catch (MessageException e) {
LOGGER.debug(String.format("%s, from peer %s", e.getMessage(), this));
this.disconnect(e.getMessage());
@@ -387,6 +404,13 @@ public class Peer {
LOGGER.trace(() -> String.format("Received %s message with ID %d from peer %s", message.getType().name(), message.getId(), this));
// Tidy up buffers:
this.byteBuffer.flip();
// Read-only, flipped buffer's position will be after end of message, so copy that
this.byteBuffer.position(readOnlyBuffer.position());
// Copy bytes after read message to front of buffer, adjusting position accordingly, reset limit to capacity
this.byteBuffer.compact();
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
if (queue != null) {
// Adding message to queue will unblock thread waiting for response
@@ -399,7 +423,7 @@ public class Peer {
// Add message to pending queue
if (!this.pendingMessages.offer(message)) {
LOGGER.info(String.format("No room to queue message from peer %s - discarding", this));
LOGGER.info(() -> String.format("No room to queue message from peer %s - discarding", this));
return;
}
@@ -454,10 +478,24 @@ public class Peer {
while (outputBuffer.hasRemaining()) {
int bytesWritten = this.socketChannel.write(outputBuffer);
LOGGER.trace(() -> String.format("Sent %d bytes of %s message with ID %d to peer %s",
bytesWritten,
message.getType().name(),
message.getId(),
this));
if (bytesWritten == 0)
// Underlying socket's internal buffer probably full,
// so wait a short while for bytes to actually be transmitted over the wire
this.socketChannel.wait(1L);
/*
* NOSONAR squid:S2276 - we don't want to use this.socketChannel.wait()
* as this releases the lock held by synchronized() above
* and would allow another thread to send another message,
* potentially interleaving them on-the-wire, causing checksum failures
* and connection loss.
*/
Thread.sleep(1L); //NOSONAR squid:S2276
}
}
} catch (MessageException e) {

View File

@@ -34,6 +34,7 @@ public class BlockMessage extends Message {
super(MessageType.BLOCK);
this.block = block;
this.blockData = block.getBlockData();
this.height = block.getBlockData().getHeight();
}
@@ -93,4 +94,10 @@ public class BlockMessage extends Message {
}
}
public BlockMessage cloneWithNewId(int newId) {
BlockMessage clone = new BlockMessage(this.block);
clone.setId(newId);
return clone;
}
}

View File

@@ -160,80 +160,72 @@ public abstract class Message {
/**
* Attempt to read a message from byte buffer.
*
* @param byteBuffer
* @param readOnlyBuffer
* @return null if no complete message can be read
* @throws MessageException
*/
public static Message fromByteBuffer(ByteBuffer byteBuffer) throws MessageException {
public static Message fromByteBuffer(ByteBuffer readOnlyBuffer) throws MessageException {
try {
byteBuffer.flip();
ByteBuffer readBuffer = byteBuffer.asReadOnlyBuffer();
// Read only enough bytes to cover Message "magic" preamble
byte[] messageMagic = new byte[MAGIC_LENGTH];
readBuffer.get(messageMagic);
readOnlyBuffer.get(messageMagic);
if (!Arrays.equals(messageMagic, Network.getInstance().getMessageMagic()))
// Didn't receive correct Message "magic"
throw new MessageException("Received incorrect message 'magic'");
// Find supporting object
int typeValue = readBuffer.getInt();
int typeValue = readOnlyBuffer.getInt();
MessageType messageType = MessageType.valueOf(typeValue);
if (messageType == null)
// Unrecognised message type
throw new MessageException(String.format("Received unknown message type [%d]", typeValue));
// Optional message ID
byte hasId = readBuffer.get();
byte hasId = readOnlyBuffer.get();
int id = -1;
if (hasId != 0) {
id = readBuffer.getInt();
id = readOnlyBuffer.getInt();
if (id <= 0)
// Invalid ID
throw new MessageException("Invalid negative ID");
}
int dataSize = readBuffer.getInt();
int dataSize = readOnlyBuffer.getInt();
if (dataSize > MAX_DATA_SIZE)
// Too large
throw new MessageException(String.format("Declared data length %d larger than max allowed %d", dataSize, MAX_DATA_SIZE));
// Don't have all the data yet?
if (dataSize > 0 && dataSize + CHECKSUM_LENGTH > readOnlyBuffer.remaining())
return null;
ByteBuffer dataSlice = null;
if (dataSize > 0) {
byte[] expectedChecksum = new byte[CHECKSUM_LENGTH];
readBuffer.get(expectedChecksum);
readOnlyBuffer.get(expectedChecksum);
// Remember this position in readBuffer so we can pass to Message subclass
dataSlice = readBuffer.slice();
// Consume data from buffer
byte[] data = new byte[dataSize];
readBuffer.get(data);
// We successfully read all the data bytes, so we can set limit on dataSlice
// Slice data in readBuffer so we can pass to Message subclass
dataSlice = readOnlyBuffer.slice();
dataSlice.limit(dataSize);
// Test checksum
byte[] actualChecksum = generateChecksum(data);
byte[] actualChecksum = generateChecksum(dataSlice);
if (!Arrays.equals(expectedChecksum, actualChecksum))
throw new MessageException("Message checksum incorrect");
// Reset position after being consumed by generateChecksum
dataSlice.position(0);
// Update position in readOnlyBuffer
readOnlyBuffer.position(readOnlyBuffer.position() + dataSize);
}
Message message = messageType.fromByteBuffer(id, dataSlice);
// We successfully read a message, so bump byteBuffer's position to reflect this
byteBuffer.position(readBuffer.position());
return message;
return messageType.fromByteBuffer(id, dataSlice);
} catch (BufferUnderflowException e) {
// Not enough bytes to fully decode message...
return null;
} finally {
byteBuffer.compact();
}
}
@@ -241,6 +233,10 @@ public abstract class Message {
return Arrays.copyOfRange(Crypto.digest(data), 0, CHECKSUM_LENGTH);
}
protected static byte[] generateChecksum(ByteBuffer dataBuffer) {
return Arrays.copyOfRange(Crypto.digest(dataBuffer), 0, CHECKSUM_LENGTH);
}
public byte[] toBytes() throws MessageException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);

View File

@@ -87,6 +87,21 @@ public interface ATRepository {
*/
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException;
/** Returns height of first trimmable AT state. */
public int getAtTrimHeight() throws DataException;
/** Sets new base height for AT state trimming.
* <p>
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
*/
public void setAtTrimHeight(int trimHeight) throws DataException;
/** Hook to allow repository to prepare/cache info for AT state trimming. */
public void prepareForAtStateTrimming() throws DataException;
/** Trims full AT state data between passed heights. Returns number of trimmed rows. */
public int trimAtStates(int minHeight, int maxHeight, int limit) throws DataException;
/**
* Save ATStateData into repository.
* <p>

View File

@@ -4,6 +4,7 @@ import java.util.List;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.EligibleQoraHolderData;
import org.qortal.data.account.MintingAccountData;
import org.qortal.data.account.QortFromQoraData;
import org.qortal.data.account.RewardShareData;
@@ -89,6 +90,13 @@ public interface AccountRepository {
*/
public int modifyMintedBlockCount(String address, int delta) throws DataException;
/**
* Modifies batch of accounts' minted block count only.
* <p>
* This is a one-shot, batch version of modifyMintedBlockCount(String, int) above.
*/
public void modifyMintedBlockCounts(List<String> addresses, int delta) throws DataException;
/** Delete account from repository. */
public void delete(String address) throws DataException;
@@ -106,6 +114,9 @@ public interface AccountRepository {
*/
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
/** Returns all account balances for given assetID, optionally excluding zero balances. */
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException;
/** How to order results when fetching asset balances. */
public enum BalanceOrdering {
/** assetID first, then balance, then account address */
@@ -116,15 +127,18 @@ public interface AccountRepository {
ASSET_ACCOUNT
}
/** Returns all account balances for given assetID, optionally excluding zero balances. */
public List<AccountBalanceData> getAssetBalances(long assetId, Boolean excludeZero) throws DataException;
/** Returns account balances for matching addresses / assetIDs, optionally excluding zero balances, with pagination, used by API. */
public List<AccountBalanceData> getAssetBalances(List<String> addresses, List<Long> assetIds, BalanceOrdering balanceOrdering, Boolean excludeZero, Integer limit, Integer offset, Boolean reverse) throws DataException;
/** Modifies account's asset balance by <tt>deltaBalance</tt>. */
public void modifyAssetBalance(String address, long assetId, long deltaBalance) throws DataException;
/** Modifies a batch of account asset balances, treating AccountBalanceData.balance as <tt>deltaBalance</tt>. */
public void modifyAssetBalances(List<AccountBalanceData> accountBalanceDeltas) throws DataException;
/** Batch update of account asset balances. */
public void setAssetBalances(List<AccountBalanceData> accountBalances) throws DataException;
public void save(AccountBalanceData accountBalanceData) throws DataException;
public void delete(String address, long assetId) throws DataException;
@@ -156,6 +170,16 @@ public interface AccountRepository {
*/
public RewardShareData getRewardShareByIndex(int index) throws DataException;
/**
* Returns list of reward-share data using array of indexes into list of reward-shares (sorted by reward-share public key).
* <p>
* This is a one-shot, batch form of the above <tt>getRewardShareByIndex(int)</tt> call.
*
* @return list of reward-share data, or null if one (or more) index is invalid
* @throws DataException
*/
public List<RewardShareData> getRewardSharesByIndexes(int[] indexes) throws DataException;
public boolean rewardShareExists(byte[] rewardSharePublicKey) throws DataException;
public void save(RewardShareData rewardShareData) throws DataException;
@@ -175,7 +199,7 @@ public interface AccountRepository {
// Managing QORT from legacy QORA
/**
* Returns balance data for accounts with legacy QORA asset that are eligible
* Returns full info for accounts with legacy QORA asset that are eligible
* for more block reward (block processing) or for block reward removal (block orphaning).
* <p>
* For block processing, accounts that have already received their final QORT reward for owning
@@ -187,7 +211,7 @@ public interface AccountRepository {
* @param blockHeight QORT reward must have be present at this height (for orphaning only)
* @throws DataException
*/
public List<AccountBalanceData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException;
public List<EligibleQoraHolderData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException;
public QortFromQoraData getQortFromQoraInfo(String address) throws DataException;

View File

@@ -143,13 +143,21 @@ public interface BlockRepository {
*/
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
/** Returns height of first trimmable online accounts signatures. */
public int getOnlineAccountsSignaturesTrimHeight() throws DataException;
/** Sets new base height for trimming online accounts signatures.
* <p>
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
*/
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException;
/**
* Trim online accounts signatures from blocks older than passed timestamp.
* Trim online accounts signatures from blocks between passed heights.
*
* @param timestamp
* @return number of blocks trimmed
*/
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException;
public int trimOldOnlineAccountsSignatures(int minHeight, int maxHeight) throws DataException;
/**
* Returns first (lowest height) block that doesn't link back to specified block.

View File

@@ -0,0 +1,31 @@
package org.qortal.repository;
import java.util.List;
import org.qortal.data.transaction.MessageTransactionData;
public interface MessageRepository {
/**
* Returns list of confirmed MESSAGE transaction data matching (some) participants.
* <p>
* At least one of <tt>senderPublicKey</tt> or <tt>recipient</tt> must be specified.
* <p>
* @throws DataException
*/
public List<MessageTransactionData> getMessagesByParticipants(byte[] senderPublicKey,
String recipient, Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Does a MESSAGE exist with matching sender (pubkey), recipient and message payload?
* <p>
* Includes both confirmed and unconfirmed transactions!
* <p>
* @param senderPublicKey
* @param recipient
* @param messageData
* @return true if a message exists, false otherwise
*/
public boolean exists(byte[] senderPublicKey, String recipient, byte[] messageData) throws DataException;
}

View File

@@ -18,6 +18,8 @@ public interface Repository extends AutoCloseable {
public GroupRepository getGroupRepository();
public MessageRepository getMessageRepository();
public NameRepository getNameRepository();
public NetworkRepository getNetworkRepository();
@@ -45,4 +47,12 @@ public interface Repository extends AutoCloseable {
public void backup(boolean quick) throws DataException;
public void checkpoint(boolean quick) throws DataException;
public void performPeriodicMaintenance() throws DataException;
public void exportNodeLocalData() throws DataException;
public void importDataFromFile(String filename) throws DataException;
}

View File

@@ -2,6 +2,8 @@ package org.qortal.repository;
public interface RepositoryFactory {
public boolean wasPristineAtOpen();
public RepositoryFactory reopen() throws DataException;
public Repository getRepository() throws DataException;

View File

@@ -8,6 +8,13 @@ public abstract class RepositoryManager {
repositoryFactory = newRepositoryFactory;
}
public static boolean wasPristineAtOpen() throws DataException {
if (repositoryFactory == null)
throw new DataException("No repository available");
return repositoryFactory.wasPristineAtOpen();
}
public static Repository getRepository() throws DataException {
if (repositoryFactory == null)
throw new DataException("No repository available");
@@ -35,6 +42,14 @@ public abstract class RepositoryManager {
}
}
public static void checkpoint(boolean quick) {
try (final Repository repository = getRepository()) {
repository.checkpoint(quick);
} catch (DataException e) {
// Checkpoint is best-effort so don't complain
}
}
public static void rebuild() throws DataException {
RepositoryFactory oldRepositoryFactory = repositoryFactory;

View File

@@ -6,7 +6,6 @@ import java.util.Map;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.data.group.GroupApprovalData;
import org.qortal.data.transaction.GroupApprovalTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.transaction.Transaction.TransactionType;
@@ -91,6 +90,22 @@ public interface TransactionRepository {
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns signatures for transactions that match search criteria.
* <p>
* Simpler version that only checks accepts one (optional) transaction type,
* and one (optional) public key, within an block height range.
*
* @param txType
* @param publicKey
* @param minBlockHeight
* @param maxBlockHeight
* @return
* @throws DataException
*/
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
Integer minBlockHeight, Integer maxBlockHeight) throws DataException;
/**
* Returns signature for latest auto-update transaction.
* <p>
@@ -108,18 +123,6 @@ public interface TransactionRepository {
*/
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
/**
* Returns list of MESSAGE transaction data matching recipient.
* @param recipient
* @param limit
* @param offset
* @param reverse
* @return
* @throws DataException
*/
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns list of transactions relating to specific asset ID.
*

View File

@@ -10,6 +10,8 @@ import org.qortal.data.at.ATStateData;
import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException;
import com.google.common.primitives.Longs;
public class HSQLDBATRepository implements ATRepository {
protected HSQLDBRepository repository;
@@ -48,7 +50,7 @@ public class HSQLDBATRepository implements ATRepository {
boolean hadFatalError = resultSet.getBoolean(10);
boolean isFrozen = resultSet.getBoolean(11);
Long frozenBalance = resultSet.getLong(11);
Long frozenBalance = resultSet.getLong(12);
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
@@ -116,7 +118,7 @@ public class HSQLDBATRepository implements ATRepository {
boolean hadFatalError = resultSet.getBoolean(10);
boolean isFrozen = resultSet.getBoolean(11);
Long frozenBalance = resultSet.getLong(11);
Long frozenBalance = resultSet.getLong(12);
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
@@ -135,16 +137,21 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public List<ATData> getATsByFunctionality(byte[] codeHash, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(512);
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ")
.append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ")
.append("is_frozen, frozen_balance ")
.append("FROM ATs ")
.append("WHERE code_hash = ? ");
bindParams.add(codeHash);
if (isExecutable != null)
sql.append("AND is_finished = ").append(isExecutable ? "false" : "true");
if (isExecutable != null) {
sql.append("AND is_finished != ? ");
bindParams.add(isExecutable);
}
sql.append(" ORDER BY created_when ");
sql.append("ORDER BY created_when ");
if (reverse != null && reverse)
sql.append("DESC");
@@ -152,7 +159,7 @@ public class HSQLDBATRepository implements ATRepository {
List<ATData> matchingATs = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), codeHash)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return matchingATs;
@@ -241,22 +248,22 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
String sql = "SELECT created_when, state_data, state_hash, fees, is_initial "
String sql = "SELECT state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE AT_address = ? AND height = ? "
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
+ "LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) {
if (resultSet == null)
return null;
long created = resultSet.getLong(1);
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
byte[] stateHash = resultSet.getBytes(3);
long fees = resultSet.getLong(4);
boolean isInitial = resultSet.getBoolean(5);
byte[] stateData = resultSet.getBytes(1); // Actually BLOB
byte[] stateHash = resultSet.getBytes(2);
long fees = resultSet.getLong(3);
boolean isInitial = resultSet.getBoolean(4);
return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT state from repository", e);
}
@@ -264,25 +271,26 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getLatestATState(String atAddress) throws DataException {
String sql = "SELECT height, created_when, state_data, state_hash, fees, is_initial "
String sql = "SELECT height, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE AT_address = ? "
+ "ORDER BY height DESC "
+ "LIMIT 1 "
+ "USING INDEX";
+ "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? "
// Order by AT_address and height to use compound primary key as index
// Both must be the same direction (DESC) also
+ "ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
+ "LIMIT 1 ";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
if (resultSet == null)
return null;
int height = resultSet.getInt(1);
long created = resultSet.getLong(2);
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
byte[] stateHash = resultSet.getBytes(4);
long fees = resultSet.getLong(5);
boolean isInitial = resultSet.getBoolean(6);
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
byte[] stateHash = resultSet.getBytes(3);
long fees = resultSet.getLong(4);
boolean isInitial = resultSet.getBoolean(5);
return new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
} catch (SQLException e) {
throw new DataException("Unable to fetch latest AT state from repository", e);
}
@@ -293,18 +301,27 @@ public class HSQLDBATRepository implements ATRepository {
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT AT_address, height, created_when, state_data, state_hash, fees, is_initial "
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial "
+ "FROM ATs "
+ "CROSS JOIN LATERAL("
+ "SELECT height, created_when, state_data, state_hash, fees, is_initial "
+ "SELECT height, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE ATStates.AT_address = ATs.AT_address "
+ "ORDER BY height DESC "
+ "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ATs.AT_address ");
if (minimumFinalHeight != null) {
sql.append("AND ATStates.height >= ? ");
bindParams.add(minimumFinalHeight);
}
// Order by AT_address and height to use compound primary key as index
// Both must be the same direction (DESC) also
sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
+ "LIMIT 1 "
+ ") AS FinalATStates "
+ "WHERE code_hash = ? ");
List<Object> bindParams = new ArrayList<>();
bindParams.add(codeHash);
if (isFinished != null) {
@@ -313,22 +330,17 @@ public class HSQLDBATRepository implements ATRepository {
}
if (dataByteOffset != null && expectedValue != null) {
sql.append("AND RAWTOHEX(SUBSTRING(state_data FROM ? FOR 8)) = ? ");
sql.append("AND SUBSTRING(state_data FROM ? FOR 8) = ? ");
// We convert our long to hex Java-side to control endian
String expectedHexValue = String.format("%016x", expectedValue); // left-zero-padding and conversion
// We convert our long on Java-side to control endian
byte[] rawExpectedValue = Longs.toByteArray(expectedValue);
// SQL binary data offsets start at 1
bindParams.add(dataByteOffset + 1);
bindParams.add(expectedHexValue);
bindParams.add(rawExpectedValue);
}
if (minimumFinalHeight != null) {
sql.append("AND height >= ");
sql.append(minimumFinalHeight);
}
sql.append(" ORDER BY height ");
sql.append(" ORDER BY FinalATStates.height ");
if (reverse != null && reverse)
sql.append("DESC");
@@ -343,13 +355,12 @@ public class HSQLDBATRepository implements ATRepository {
do {
String atAddress = resultSet.getString(1);
int height = resultSet.getInt(2);
long created = resultSet.getLong(3);
byte[] stateData = resultSet.getBytes(4); // Actually BLOB
byte[] stateHash = resultSet.getBytes(5);
long fees = resultSet.getLong(6);
boolean isInitial = resultSet.getBoolean(7);
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
byte[] stateHash = resultSet.getBytes(4);
long fees = resultSet.getLong(5);
boolean isInitial = resultSet.getBoolean(6);
ATStateData atStateData = new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial);
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
atStates.add(atStateData);
} while (resultSet.next());
@@ -363,8 +374,10 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
String sql = "SELECT AT_address, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE height = ? "
+ "FROM ATs "
+ "LEFT OUTER JOIN ATStates "
+ "ON ATStates.AT_address = ATs.AT_address AND height = ? "
+ "WHERE ATStates.AT_address IS NOT NULL "
+ "ORDER BY created_when ASC";
List<ATStateData> atStates = new ArrayList<>();
@@ -391,29 +404,131 @@ public class HSQLDBATRepository implements ATRepository {
}
@Override
public void save(ATStateData atStateData) throws DataException {
// We shouldn't ever save partial ATStateData
if (atStateData.getCreation() == null || atStateData.getStateHash() == null || atStateData.getHeight() == null)
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
public int getAtTrimHeight() throws DataException {
String sql = "SELECT AT_trim_height FROM DatabaseInfo";
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
return 0;
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("created_when", atStateData.getCreation()).bind("state_data", atStateData.getStateData())
.bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees())
.bind("is_initial", atStateData.isInitial());
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT state trim height from repository", e);
}
}
@Override
public void setAtTrimHeight(int trimHeight) throws DataException {
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
// that could result in "transaction rollback: serialization failure"
synchronized (this.repository.trimHeightsLock) {
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
try {
this.repository.executeCheckedUpdate(updateSql, trimHeight);
this.repository.saveChanges();
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to set AT state trim height in repository", e);
}
}
}
@Override
public void prepareForAtStateTrimming() throws DataException {
// Rebuild cache of latest AT states that we can't trim
String deleteSql = "DELETE FROM LatestATStates";
try {
this.repository.executeCheckedUpdate(deleteSql);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to delete temporary latest AT states cache from repository", e);
}
String insertSql = "INSERT INTO LatestATStates ("
+ "SELECT AT_address, height FROM ATs "
+ "CROSS JOIN LATERAL("
+ "SELECT height FROM ATStates "
+ "WHERE ATStates.AT_address = ATs.AT_address "
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
+ ") "
+ ")";
try {
this.repository.executeCheckedUpdate(insertSql);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);
}
}
@Override
public int trimAtStates(int minHeight, int maxHeight, int limit) throws DataException {
if (minHeight >= maxHeight)
return 0;
// We're often called so no need to trim all states in one go.
// Limit updates to reduce CPU and memory load.
String sql = "DELETE FROM ATStatesData "
+ "WHERE height BETWEEN ? AND ? "
+ "AND NOT EXISTS("
+ "SELECT TRUE FROM LatestATStates "
+ "WHERE LatestATStates.AT_address = ATStatesData.AT_address "
+ "AND LatestATStates.height = ATStatesData.height"
+ ") "
+ "LIMIT ?";
try {
saveHelper.execute(this.repository);
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight, limit);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to trim AT states in repository", e);
}
}
@Override
public void save(ATStateData atStateData) throws DataException {
// We shouldn't ever save partial ATStateData
if (atStateData.getStateHash() == null || atStateData.getHeight() == null)
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
HSQLDBSaver atStatesSaver = new HSQLDBSaver("ATStates");
atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("state_hash", atStateData.getStateHash())
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial());
try {
atStatesSaver.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save AT state into repository", e);
}
if (atStateData.getStateData() != null) {
HSQLDBSaver atStatesDataSaver = new HSQLDBSaver("ATStatesData");
atStatesDataSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("state_data", atStateData.getStateData());
try {
atStatesDataSaver.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save AT state data into repository", e);
}
} else {
try {
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?",
atStateData.getATAddress(), atStateData.getHeight());
} catch (SQLException e) {
throw new DataException("Unable to delete AT state data from repository", e);
}
}
}
@Override
public void delete(String atAddress, int height) throws DataException {
try {
this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
this.repository.delete("ATStatesData", "AT_address = ? AND height = ?", atAddress, height);
} catch (SQLException e) {
throw new DataException("Unable to delete AT state from repository", e);
}
@@ -423,6 +538,7 @@ public class HSQLDBATRepository implements ATRepository {
public void deleteATStates(int height) throws DataException {
try {
this.repository.delete("ATStates", "height = ?", height);
this.repository.delete("ATStatesData", "height = ?", height);
} catch (SQLException e) {
throw new DataException("Unable to delete AT states from repository", e);
}

View File

@@ -1,13 +1,17 @@
package org.qortal.repository.hsqldb;
import static org.qortal.utils.Amounts.prettyAmount;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.qortal.asset.Asset;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.EligibleQoraHolderData;
import org.qortal.data.account.MintingAccountData;
import org.qortal.data.account.QortFromQoraData;
import org.qortal.data.account.RewardShareData;
@@ -145,7 +149,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
public void ensureAccount(AccountData accountData) throws DataException {
String sql = "INSERT IGNORE INTO Accounts (account, public_key) VALUES (?, ?)"; // MySQL syntax
try {
this.repository.checkedExecuteUpdateCount(sql, accountData.getAddress(), accountData.getPublicKey());
this.repository.executeCheckedUpdate(sql, accountData.getAddress(), accountData.getPublicKey());
} catch (SQLException e) {
throw new DataException("Unable to ensure minimal account in repository", e);
}
@@ -260,12 +264,26 @@ public class HSQLDBAccountRepository implements AccountRepository {
"ON DUPLICATE KEY UPDATE blocks_minted = blocks_minted + ?";
try {
return this.repository.checkedExecuteUpdateCount(sql, address, delta, delta);
return this.repository.executeCheckedUpdate(sql, address, delta, delta);
} catch (SQLException e) {
throw new DataException("Unable to modify account's minted block count in repository", e);
}
}
@Override
public void modifyMintedBlockCounts(List<String> addresses, int delta) throws DataException {
String sql = "INSERT INTO Accounts (account, blocks_minted) VALUES (?, ?) " +
"ON DUPLICATE KEY UPDATE blocks_minted = blocks_minted + ?";
List<Object[]> bindParamRows = addresses.stream().map(address -> new Object[] { address, delta, delta }).collect(Collectors.toList());
try {
this.repository.executeCheckedBatchUpdate(sql, bindParamRows);
} catch (SQLException e) {
throw new DataException("Unable to modify many account minted block counts in repository", e);
}
}
@Override
public void delete(String address) throws DataException {
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
@@ -447,7 +465,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
// Perform actual balance change
String sql = "UPDATE AccountBalances set balance = balance + ? WHERE account = ? AND asset_id = ?";
try {
this.repository.checkedExecuteUpdateCount(sql, deltaBalance, address, assetId);
this.repository.executeCheckedUpdate(sql, deltaBalance, address, assetId);
} catch (SQLException e) {
throw new DataException("Unable to reduce account balance in repository", e);
}
@@ -455,7 +473,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
// We have to ensure parent row exists to satisfy foreign key constraint
try {
String sql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
this.repository.checkedExecuteUpdateCount(sql, address);
this.repository.executeCheckedUpdate(sql, address);
} catch (SQLException e) {
throw new DataException("Unable to ensure minimal account in repository", e);
}
@@ -464,13 +482,95 @@ public class HSQLDBAccountRepository implements AccountRepository {
String sql = "INSERT INTO AccountBalances (account, asset_id, balance) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE balance = balance + ?";
try {
this.repository.checkedExecuteUpdateCount(sql, address, assetId, deltaBalance, deltaBalance);
this.repository.executeCheckedUpdate(sql, address, assetId, deltaBalance, deltaBalance);
} catch (SQLException e) {
throw new DataException("Unable to increase account balance in repository", e);
}
}
}
public void modifyAssetBalances(List<AccountBalanceData> accountBalanceDeltas) throws DataException {
// Nothing to do?
if (accountBalanceDeltas == null || accountBalanceDeltas.isEmpty())
return;
// Map balance changes into SQL bind params, filtering out no-op changes
List<Object[]> modifyBalanceParams = accountBalanceDeltas.stream()
.filter(accountBalance -> accountBalance.getBalance() != 0L)
.map(accountBalance -> new Object[] { accountBalance.getAddress(), accountBalance.getAssetId(), accountBalance.getBalance(), accountBalance.getBalance() })
.collect(Collectors.toList());
// Before we modify balances, ensure parent accounts exist
String ensureSql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
try {
this.repository.executeCheckedBatchUpdate(ensureSql, modifyBalanceParams.stream().map(objects -> new Object[] { objects[0] }).collect(Collectors.toList()));
} catch (SQLException e) {
throw new DataException("Unable to ensure minimal accounts in repository", e);
}
// Perform actual balance changes
String sql = "INSERT INTO AccountBalances (account, asset_id, balance) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE balance = balance + ?";
try {
this.repository.executeCheckedBatchUpdate(sql, modifyBalanceParams);
} catch (SQLException e) {
throw new DataException("Unable to modify account balances in repository", e);
}
}
@Override
public void setAssetBalances(List<AccountBalanceData> accountBalances) throws DataException {
// Nothing to do?
if (accountBalances == null || accountBalances.isEmpty())
return;
/*
* Split workload into zero and non-zero balances,
* checking for negative balances as we progress.
*/
List<Object[]> zeroAccountBalanceParams = new ArrayList<>();
List<Object[]> nonZeroAccountBalanceParams = new ArrayList<>();
for (AccountBalanceData accountBalanceData : accountBalances) {
final long balance = accountBalanceData.getBalance();
if (balance < 0)
throw new DataException(String.format("Refusing to set negative balance %s [assetId %d] for %s",
prettyAmount(balance), accountBalanceData.getAssetId(), accountBalanceData.getAddress()));
if (balance == 0)
zeroAccountBalanceParams.add(new Object[] { accountBalanceData.getAddress(), accountBalanceData.getAssetId() });
else
nonZeroAccountBalanceParams.add(new Object[] { accountBalanceData.getAddress(), accountBalanceData.getAssetId(), balance, balance });
}
// Batch update (actually delete) of zero balances
try {
this.repository.deleteBatch("AccountBalances", "account = ? AND asset_id = ?", zeroAccountBalanceParams);
} catch (SQLException e) {
throw new DataException("Unable to delete account balances from repository", e);
}
// Before we set new balances, ensure parent accounts exist
String ensureSql = "INSERT IGNORE INTO Accounts (account) VALUES (?)"; // MySQL syntax
try {
this.repository.executeCheckedBatchUpdate(ensureSql, nonZeroAccountBalanceParams.stream().map(objects -> new Object[] { objects[0] }).collect(Collectors.toList()));
} catch (SQLException e) {
throw new DataException("Unable to ensure minimal accounts in repository", e);
}
// Now set all balances in one go
String setSql = "INSERT INTO AccountBalances (account, asset_id, balance) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE balance = ?";
try {
this.repository.executeCheckedBatchUpdate(setSql, nonZeroAccountBalanceParams);
} catch (SQLException e) {
throw new DataException("Unable to set account balances in repository", e);
}
}
@Override
public void save(AccountBalanceData accountBalanceData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");
@@ -699,7 +799,52 @@ public class HSQLDBAccountRepository implements AccountRepository {
return new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
} catch (SQLException e) {
throw new DataException("Unable to fetch reward-share info from repository", e);
throw new DataException("Unable to fetch indexed reward-share from repository", e);
}
}
@Override
public List<RewardShareData> getRewardSharesByIndexes(int[] indexes) throws DataException {
String sql = "SELECT minter_public_key, minter, recipient, share_percent, reward_share_public_key FROM RewardShares "
+ "ORDER BY reward_share_public_key ASC";
if (indexes == null)
return null;
List<RewardShareData> rewardShares = new ArrayList<>();
if (indexes.length == 0)
return rewardShares;
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
return null;
int rowNum = 1;
for (int i = 0; i < indexes.length; ++i) {
final int index = indexes[i];
while (rowNum < index + 1) { // +1 because in JDBC, first row is row 1
if (!resultSet.next())
// Index is out of bounds
return null;
++rowNum;
}
byte[] minterPublicKey = resultSet.getBytes(1);
String minter = resultSet.getString(2);
String recipient = resultSet.getString(3);
int sharePercent = resultSet.getInt(4);
byte[] rewardSharePublicKey = resultSet.getBytes(5);
RewardShareData rewardShareData = new RewardShareData(minterPublicKey, minter, recipient, rewardSharePublicKey, sharePercent);
rewardShares.add(rewardShareData);
}
return rewardShares;
} catch (SQLException e) {
throw new DataException("Unable to fetch indexed reward-shares from repository", e);
}
}
@@ -785,35 +930,49 @@ public class HSQLDBAccountRepository implements AccountRepository {
// Managing QORT from legacy QORA
@Override
public List<AccountBalanceData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException {
public List<EligibleQoraHolderData> getEligibleLegacyQoraHolders(Integer blockHeight) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT account, balance from AccountBalances ");
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT account, Qora.balance, QortFromQora.balance, final_qort_from_qora, final_block_height ");
sql.append("FROM AccountBalances AS Qora ");
sql.append("LEFT OUTER JOIN AccountQortFromQoraInfo USING (account) ");
sql.append("WHERE asset_id = ");
sql.append("LEFT OUTER JOIN AccountBalances AS QortFromQora ON QortFromQora.account = Qora.account AND QortFromQora.asset_id = ");
sql.append(Asset.QORT_FROM_QORA); // int is safe to use literally
sql.append(" WHERE Qora.asset_id = ");
sql.append(Asset.LEGACY_QORA); // int is safe to use literally
sql.append(" AND (final_block_height IS NULL");
if (blockHeight != null) {
sql.append(" OR final_block_height >= ");
sql.append(blockHeight);
sql.append(" OR final_block_height >= ?");
bindParams.add(blockHeight);
}
sql.append(")");
List<AccountBalanceData> accountBalances = new ArrayList<>();
List<EligibleQoraHolderData> eligibleLegacyQoraHolders = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return accountBalances;
return eligibleLegacyQoraHolders;
do {
String address = resultSet.getString(1);
long balance = resultSet.getLong(2);
long qoraBalance = resultSet.getLong(2);
long qortFromQoraBalance = resultSet.getLong(3);
accountBalances.add(new AccountBalanceData(address, Asset.LEGACY_QORA, balance));
Long finalQortFromQora = resultSet.getLong(4);
if (finalQortFromQora == 0 && resultSet.wasNull())
finalQortFromQora = null;
Integer finalBlockHeight = resultSet.getInt(5);
if (finalBlockHeight == 0 && resultSet.wasNull())
finalBlockHeight = null;
eligibleLegacyQoraHolders.add(new EligibleQoraHolderData(address, qoraBalance, qortFromQoraBalance, finalQortFromQora, finalBlockHeight));
} while (resultSet.next());
return accountBalances;
return eligibleLegacyQoraHolders;
} catch (SQLException e) {
throw new DataException("Unable to fetch eligible legacy QORA holders from repository", e);
}

View File

@@ -120,7 +120,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public int getHeightFromTimestamp(long timestamp) throws DataException {
// Uses (minted_when, height) index
String sql = "SELECT height FROM Blocks WHERE minted_when <= ? ORDER BY minted_when DESC LIMIT 1";
String sql = "SELECT height FROM Blocks WHERE minted_when <= ? ORDER BY minted_when DESC, height DESC LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, timestamp)) {
if (resultSet == null)
@@ -175,7 +175,11 @@ public class HSQLDBBlockRepository implements BlockRepository {
public List<TransactionData> getTransactionsFromSignature(byte[] signature, Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(256);
sql.append("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? ORDER BY sequence");
sql.append("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ? ORDER BY block_signature");
if (reverse != null && reverse)
sql.append(" DESC");
sql.append(", sequence");
if (reverse != null && reverse)
sql.append(" DESC");
@@ -378,6 +382,8 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException {
StringBuilder sql = new StringBuilder(512);
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT signature, height, minted_when, transaction_count, RewardShares.minter ");
/*
@@ -396,10 +402,9 @@ public class HSQLDBBlockRepository implements BlockRepository {
if (startHeight != null && endHeight != null) {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(startHeight);
sql.append(" AND ");
sql.append(endHeight - 1);
sql.append("WHERE height BETWEEN ? AND ?");
bindParams.add(startHeight);
bindParams.add(Integer.valueOf(endHeight - 1));
} else if (endHeight != null || (startHeight == null && count != null)) {
// we are going to return blocks from the end of the chain
if (count == null)
@@ -407,17 +412,15 @@ public class HSQLDBBlockRepository implements BlockRepository {
if (endHeight == null) {
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
sql.append("JOIN Blocks ON height BETWEEN (max_height - ");
sql.append(count);
sql.append(" + 1) AND max_height ");
sql.append("JOIN Blocks ON height BETWEEN (max_height - ? + 1) AND max_height ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter");
bindParams.add(count);
} else {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(endHeight - count);
sql.append(" AND ");
sql.append(endHeight - 1);
sql.append("WHERE height BETWEEN ? AND ?");
bindParams.add(Integer.valueOf(endHeight - count));
bindParams.add(Integer.valueOf(endHeight - 1));
}
} else {
@@ -430,15 +433,14 @@ public class HSQLDBBlockRepository implements BlockRepository {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(startHeight);
sql.append(" AND ");
sql.append(startHeight + count - 1);
sql.append("WHERE height BETWEEN ? AND ?");
bindParams.add(startHeight);
bindParams.add(Integer.valueOf(startHeight + count - 1));
}
List<BlockInfo> blockInfos = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return blockInfos;
@@ -460,12 +462,48 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
@Override
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException {
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL";
public int getOnlineAccountsSignaturesTrimHeight() throws DataException {
String sql = "SELECT online_signatures_trim_height FROM DatabaseInfo";
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
return 0;
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Unable to fetch online accounts signatures trim height from repository", e);
}
}
@Override
public void setOnlineAccountsSignaturesTrimHeight(int trimHeight) throws DataException {
// trimHeightsLock is to prevent concurrent update on DatabaseInfo
// that could result in "transaction rollback: serialization failure"
synchronized (this.repository.trimHeightsLock) {
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
try {
this.repository.executeCheckedUpdate(updateSql, trimHeight);
this.repository.saveChanges();
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to set online accounts signatures trim height in repository", e);
}
}
}
@Override
public int trimOldOnlineAccountsSignatures(int minHeight, int maxHeight) throws DataException {
// We're often called so no need to trim all blocks in one go.
// Limit updates to reduce CPU and memory load.
String sql = "UPDATE Blocks SET online_accounts_signatures = NULL "
+ "WHERE online_accounts_signatures IS NOT NULL "
+ "AND height BETWEEN ? AND ?";
try {
return this.repository.checkedExecuteUpdateCount(sql, timestamp);
return this.repository.executeCheckedUpdate(sql, minHeight, maxHeight);
} catch (SQLException e) {
repository.examineException(e);
throw new DataException("Unable to trim old online accounts signatures in repository", e);
}
}

View File

@@ -18,11 +18,16 @@ public class HSQLDBDatabaseUpdates {
/**
* Apply any incremental changes to database schema.
*
* @return true if database was non-existent/empty, false otherwise
* @throws SQLException
*/
public static void updateDatabase(Connection connection) throws SQLException {
while (databaseUpdating(connection))
public static boolean updateDatabase(Connection connection) throws SQLException {
final boolean wasPristine = fetchDatabaseVersion(connection) == 0;
while (databaseUpdating(connection, wasPristine))
incrementDatabaseVersion(connection);
return wasPristine;
}
/**
@@ -40,23 +45,21 @@ public class HSQLDBDatabaseUpdates {
/**
* Fetch current version of database schema.
*
* @return int, 0 if no schema yet
* @return database version, or 0 if no schema yet
* @throws SQLException
*/
private static int fetchDatabaseVersion(Connection connection) throws SQLException {
int databaseVersion = 0;
try (Statement stmt = connection.createStatement()) {
if (stmt.execute("SELECT version FROM DatabaseInfo"))
try (ResultSet resultSet = stmt.getResultSet()) {
if (resultSet.next())
databaseVersion = resultSet.getInt(1);
return resultSet.getInt(1);
}
} catch (SQLException e) {
// empty database
}
return databaseVersion;
return 0;
}
/**
@@ -65,7 +68,7 @@ public class HSQLDBDatabaseUpdates {
* @return true - if a schema update happened, false otherwise
* @throws SQLException
*/
private static boolean databaseUpdating(Connection connection) throws SQLException {
private static boolean databaseUpdating(Connection connection, boolean wasPristine) throws SQLException {
int databaseVersion = fetchDatabaseVersion(connection);
try (Statement stmt = connection.createStatement()) {
@@ -141,7 +144,7 @@ public class HSQLDBDatabaseUpdates {
+ "transaction_count INTEGER NOT NULL, total_fees QortalAmount NOT NULL, transactions_signature Signature NOT NULL, "
+ "height INTEGER NOT NULL, minted_when EpochMillis NOT NULL, "
+ "minter QortalPublicKey NOT NULL, minter_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QortalAmount NOT NULL, "
+ "online_accounts VARBINARY(1204), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures VARBINARY(1M), "
+ "online_accounts VARBINARY(1024), online_accounts_count INTEGER NOT NULL, online_accounts_timestamp EpochMillis, online_accounts_signatures VARBINARY(1M), "
+ "PRIMARY KEY (signature))");
// For finding blocks by height.
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
@@ -153,16 +156,6 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX BlockTimestampHeightIndex ON Blocks (minted_when, height)");
// Use a separate table space as this table will be very large.
stmt.execute("SET TABLE Blocks NEW SPACE");
// Table to hold next block height.
stmt.execute("CREATE TABLE NextBlockHeight (height INT NOT NULL)");
// Initial value - should work for empty DB or populated DB.
stmt.execute("INSERT INTO NextBlockHeight VALUES (SELECT IFNULL(MAX(height), 0) + 1 FROM Blocks)");
// We use triggers on Blocks to update a simple "next block height" table
String blockUpdateSql = "UPDATE NextBlockHeight SET height = (SELECT height + 1 FROM Blocks ORDER BY height DESC LIMIT 1)";
stmt.execute("CREATE TRIGGER Next_block_height_insert_trigger AFTER INSERT ON Blocks " + blockUpdateSql);
stmt.execute("CREATE TRIGGER Next_block_height_update_trigger AFTER UPDATE ON Blocks " + blockUpdateSql);
stmt.execute("CREATE TRIGGER Next_block_height_delete_trigger AFTER DELETE ON Blocks " + blockUpdateSql);
break;
case 2:
@@ -222,6 +215,8 @@ public class HSQLDBDatabaseUpdates {
+ "PRIMARY KEY (account))");
// For looking up an account by public key
stmt.execute("CREATE INDEX AccountPublicKeyIndex on Accounts (public_key)");
// Use a separate table space as this table will be very large.
stmt.execute("SET TABLE Accounts NEW SPACE");
// Account balances
stmt.execute("CREATE TABLE AccountBalances (account QortalAddress, asset_id AssetID, balance QortalAmount NOT NULL, "
@@ -230,6 +225,8 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX AccountBalancesAssetBalanceIndex ON AccountBalances (asset_id, balance)");
// Add CHECK constraint to account balances
stmt.execute("ALTER TABLE AccountBalances ADD CONSTRAINT CheckBalanceNotNegative CHECK (balance >= 0)");
// Use a separate table space as this table will be very large.
stmt.execute("SET TABLE AccountBalances NEW SPACE");
// Keeping track of QORT gained from holding legacy QORA
stmt.execute("CREATE TABLE AccountQortFromQoraInfo (account QortalAddress, final_qort_from_qora QortalAmount, final_block_height INT, "
@@ -427,6 +424,8 @@ public class HSQLDBDatabaseUpdates {
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
// For finding per-block AT states, ordered by creation timestamp
stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, created_when)");
// Use a separate table space as this table will be very large.
stmt.execute("SET TABLE ATStates NEW SPACE");
// Deploy CIYAM AT Transactions
stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QortalPublicKey NOT NULL, AT_name ATName NOT NULL, "
@@ -650,6 +649,128 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CHECKPOINT");
break;
case 23:
// MESSAGE transactions index
stmt.execute("CREATE INDEX IF NOT EXISTS MessageTransactionsRecipientIndex ON MessageTransactions (recipient, sender)");
break;
case 24:
// Remove unused NextBlockHeight table and corresponding triggers
stmt.execute("DROP TRIGGER IF EXISTS Next_block_height_insert_trigger");
stmt.execute("DROP TRIGGER IF EXISTS Next_block_height_update_trigger");
stmt.execute("DROP TRIGGER IF EXISTS Next_block_height_delete_trigger");
stmt.execute("DROP TABLE IF EXISTS NextBlockHeight");
break;
case 25:
// DISABLED: improved version in case 30!
// Remove excess created_when from ATStates
// stmt.execute("ALTER TABLE ATStates DROP created_when");
// stmt.execute("CREATE INDEX ATStateHeightIndex on ATStates (height)");
break;
case 26:
// Support for trimming
stmt.execute("ALTER TABLE DatabaseInfo ADD AT_trim_height INT NOT NULL DEFAULT 0");
stmt.execute("ALTER TABLE DatabaseInfo ADD online_signatures_trim_height INT NOT NULL DEFAULT 0");
break;
case 27:
// More indexes
stmt.execute("CREATE INDEX IF NOT EXISTS PaymentTransactionsRecipientIndex ON PaymentTransactions (recipient)");
stmt.execute("CREATE INDEX IF NOT EXISTS ATTransactionsRecipientIndex ON ATTransactions (recipient)");
break;
case 28:
// Latest AT state cache
stmt.execute("CREATE TEMPORARY TABLE IF NOT EXISTS LatestATStates ("
+ "AT_address QortalAddress NOT NULL, "
+ "height INT NOT NULL"
+ ")");
break;
case 29:
// Turn off HSQLDB redo-log "blockchain.log" and periodically call "CHECKPOINT" ourselves
stmt.execute("SET FILES LOG FALSE");
stmt.execute("CHECKPOINT");
break;
case 30:
// Split AT state data off to new table for better performance/management.
if (!wasPristine && !"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) {
// First, backup node-local data in case user wants to avoid long reshape and use bootstrap instead
try (ResultSet resultSet = stmt.executeQuery("SELECT COUNT(*) FROM MintingAccounts")) {
int rowCount = resultSet.next() ? resultSet.getInt(1) : 0;
if (rowCount > 0) {
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'");
LOGGER.info("Exported sensitive/node-local minting keys into MintingAccounts.script");
}
}
try (ResultSet resultSet = stmt.executeQuery("SELECT COUNT(*) FROM TradeBotStates")) {
int rowCount = resultSet.next() ? resultSet.getInt(1) : 0;
if (rowCount > 0) {
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'");
LOGGER.info("Exported sensitive/node-local trade-bot states into TradeBotStates.script");
}
}
LOGGER.info("If following reshape takes too long, use bootstrap and import node-local data using API's POST /admin/repository/data");
}
// Create new AT-states table without full state data
stmt.execute("CREATE TABLE ATStatesNew ("
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, "
+ "PRIMARY KEY (AT_address, height), "
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
stmt.execute("SET TABLE ATStatesNew NEW SPACE");
stmt.execute("CHECKPOINT");
ResultSet resultSet = stmt.executeQuery("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1");
final int blockchainHeight = resultSet.next() ? resultSet.getInt(1) : 0;
final int heightStep = 100;
LOGGER.info("Rebuilding AT state summaries in repository - this might take a while... (approx. 2 mins on high-spec)");
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
stmt.execute("INSERT INTO ATStatesNew ("
+ "SELECT AT_address, height, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
+ ")");
stmt.execute("COMMIT");
}
stmt.execute("CHECKPOINT");
LOGGER.info("Rebuilding AT states height index in repository - this might take about 3x longer...");
stmt.execute("CREATE INDEX ATStatesHeightIndex ON ATStatesNew (height)");
stmt.execute("CHECKPOINT");
stmt.execute("CREATE TABLE ATStatesData ("
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_data ATState NOT NULL, "
+ "PRIMARY KEY (height, AT_address), "
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
stmt.execute("SET TABLE ATStatesData NEW SPACE");
stmt.execute("CHECKPOINT");
LOGGER.info("Rebuilding AT state data in repository - this might take a while... (approx. 2 mins on high-spec)");
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
stmt.execute("INSERT INTO ATStatesData ("
+ "SELECT AT_address, height, state_data "
+ "FROM ATstates "
+ "WHERE state_data IS NOT NULL "
+ "AND height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
+ ")");
stmt.execute("COMMIT");
}
stmt.execute("CHECKPOINT");
stmt.execute("DROP TABLE ATStates");
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
stmt.execute("CHECKPOINT");
break;
default:
// nothing to do
return false;

View File

@@ -0,0 +1,85 @@
package org.qortal.repository.hsqldb;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.MessageRepository;
import org.qortal.transaction.Transaction.TransactionType;
public class HSQLDBMessageRepository implements MessageRepository {
protected HSQLDBRepository repository;
public HSQLDBMessageRepository(HSQLDBRepository repository) {
this.repository = repository;
}
@Override
public List<MessageTransactionData> getMessagesByParticipants(byte[] senderPublicKey,
String recipient, Integer limit, Integer offset, Boolean reverse) throws DataException {
if (senderPublicKey == null && recipient == null)
throw new DataException("At least one of senderPublicKey or recipient required to fetch matching messages");
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<>();
List<Object> bindParams = new ArrayList<>();
if (senderPublicKey != null) {
whereClauses.add("sender = ?");
bindParams.add(senderPublicKey);
}
if (recipient != null) {
whereClauses.add("recipient = ?");
bindParams.add(recipient);
}
sql.append(String.join(" AND ", whereClauses));
sql.append("ORDER BY Transactions.created_when");
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return messageTransactionsData;
do {
byte[] signature = resultSet.getBytes(1);
TransactionData transactionData = this.repository.getTransactionRepository().fromSignature(signature);
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
throw new DataException("Inconsistent data from repository when fetching message");
messageTransactionsData.add((MessageTransactionData) transactionData);
} while (resultSet.next());
return messageTransactionsData;
} catch (SQLException e) {
throw new DataException("Unable to fetch matching messages from repository", e);
}
}
@Override
public boolean exists(byte[] senderPublicKey, String recipient, byte[] messageData) throws DataException {
try {
return this.repository.exists("MessageTransactions", "sender = ? AND recipient = ? AND data = ?", senderPublicKey, recipient, messageData);
} catch (SQLException e) {
throw new DataException("Unable to check for existing message in repository", e);
}
}
}

View File

@@ -16,11 +16,15 @@ import java.sql.Savepoint;
import java.sql.Statement;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -35,6 +39,7 @@ import org.qortal.repository.ChatRepository;
import org.qortal.repository.CrossChainRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import org.qortal.repository.MessageRepository;
import org.qortal.repository.NameRepository;
import org.qortal.repository.NetworkRepository;
import org.qortal.repository.Repository;
@@ -49,18 +54,33 @@ public class HSQLDBRepository implements Repository {
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
protected Connection connection;
protected Deque<Savepoint> savepoints;
protected final Deque<Savepoint> savepoints = new ArrayDeque<>(3);
protected boolean debugState = false;
protected Long slowQueryThreshold = null;
protected List<String> sqlStatements;
protected long sessionId;
protected final Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
protected final Object trimHeightsLock = new Object();
private final ATRepository atRepository = new HSQLDBATRepository(this);
private final AccountRepository accountRepository = new HSQLDBAccountRepository(this);
private final ArbitraryRepository arbitraryRepository = new HSQLDBArbitraryRepository(this);
private final AssetRepository assetRepository = new HSQLDBAssetRepository(this);
private final BlockRepository blockRepository = new HSQLDBBlockRepository(this);
private final ChatRepository chatRepository = new HSQLDBChatRepository(this);
private final CrossChainRepository crossChainRepository = new HSQLDBCrossChainRepository(this);
private final GroupRepository groupRepository = new HSQLDBGroupRepository(this);
private final MessageRepository messageRepository = new HSQLDBMessageRepository(this);
private final NameRepository nameRepository = new HSQLDBNameRepository(this);
private final NetworkRepository networkRepository = new HSQLDBNetworkRepository(this);
private final TransactionRepository transactionRepository = new HSQLDBTransactionRepository(this);
private final VotingRepository votingRepository = new HSQLDBVotingRepository(this);
// Constructors
// NB: no visibility modifier so only callable from within same package
/* package */ HSQLDBRepository(Connection connection) throws DataException {
this.connection = connection;
this.savepoints = new ArrayDeque<>(3);
this.slowQueryThreshold = Settings.getInstance().getSlowQueryThreshold();
if (this.slowQueryThreshold != null)
@@ -88,62 +108,67 @@ public class HSQLDBRepository implements Repository {
@Override
public ATRepository getATRepository() {
return new HSQLDBATRepository(this);
return this.atRepository;
}
@Override
public AccountRepository getAccountRepository() {
return new HSQLDBAccountRepository(this);
return this.accountRepository;
}
@Override
public ArbitraryRepository getArbitraryRepository() {
return new HSQLDBArbitraryRepository(this);
return this.arbitraryRepository;
}
@Override
public AssetRepository getAssetRepository() {
return new HSQLDBAssetRepository(this);
return this.assetRepository;
}
@Override
public BlockRepository getBlockRepository() {
return new HSQLDBBlockRepository(this);
return this.blockRepository;
}
@Override
public ChatRepository getChatRepository() {
return new HSQLDBChatRepository(this);
return this.chatRepository;
}
@Override
public CrossChainRepository getCrossChainRepository() {
return new HSQLDBCrossChainRepository(this);
return this.crossChainRepository;
}
@Override
public GroupRepository getGroupRepository() {
return new HSQLDBGroupRepository(this);
return this.groupRepository;
}
@Override
public MessageRepository getMessageRepository() {
return this.messageRepository;
}
@Override
public NameRepository getNameRepository() {
return new HSQLDBNameRepository(this);
return this.nameRepository;
}
@Override
public NetworkRepository getNetworkRepository() {
return new HSQLDBNetworkRepository(this);
return this.networkRepository;
}
@Override
public TransactionRepository getTransactionRepository() {
return new HSQLDBTransactionRepository(this);
return this.transactionRepository;
}
@Override
public VotingRepository getVotingRepository() {
return new HSQLDBVotingRepository(this);
return this.votingRepository;
}
@Override
@@ -160,8 +185,20 @@ public class HSQLDBRepository implements Repository {
@Override
public void saveChanges() throws DataException {
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
try {
this.connection.commit();
if (this.slowQueryThreshold != null) {
long queryTime = System.currentTimeMillis() - beforeQuery;
if (queryTime > this.slowQueryThreshold) {
LOGGER.info(() -> String.format("[Session %d] HSQLDB COMMIT took %d ms", this.sessionId, queryTime), new SQLException("slow commit"));
logStatements();
}
}
} catch (SQLException e) {
throw new DataException("commit error", e);
} finally {
@@ -185,7 +222,7 @@ public class HSQLDBRepository implements Repository {
this.savepoints.clear();
// Before clearing statements so we can log what led to assertion error
assertEmptyTransaction("transaction commit");
assertEmptyTransaction("transaction rollback");
if (this.sqlStatements != null)
this.sqlStatements.clear();
@@ -240,7 +277,12 @@ public class HSQLDBRepository implements Repository {
try (Statement stmt = this.connection.createStatement()) {
assertEmptyTransaction("connection close");
// give connection back to the pool
// Assume we are not going to be GC'd for a while
this.preparedStatementCache.clear();
this.sqlStatements = null;
this.savepoints.clear();
// Give connection back to the pool
this.connection.close();
this.connection = null;
} catch (SQLException e) {
@@ -270,11 +312,12 @@ public class HSQLDBRepository implements Repository {
Path oldRepoDirPath = Paths.get(dbPathname).getParent();
// Delete old repository files
Files.walk(oldRepoDirPath)
.sorted(Comparator.reverseOrder())
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
paths.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.filter(file -> file.getPath().startsWith(dbPathname))
.forEach(File::delete);
}
}
} catch (NoSuchFileException e) {
// Nothing to remove
@@ -314,11 +357,12 @@ public class HSQLDBRepository implements Repository {
Path backupDirPath = Paths.get(backupPathname).getParent();
String backupDirPathname = backupDirPath.toString();
Files.walk(backupDirPath)
.sorted(Comparator.reverseOrder())
try (Stream<Path> paths = Files.walk(backupDirPath)) {
paths.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.filter(file -> file.getPath().startsWith(backupDirPathname))
.forEach(File::delete);
}
} catch (NoSuchFileException e) {
// Nothing to remove
} catch (SQLException | IOException e) {
@@ -333,8 +377,56 @@ public class HSQLDBRepository implements Repository {
}
}
@Override
public void checkpoint(boolean quick) throws DataException {
try (Statement stmt = this.connection.createStatement()) {
stmt.execute(quick ? "CHECKPOINT" : "CHECKPOINT DEFRAG");
} catch (SQLException e) {
throw new DataException("Unable to perform repository checkpoint");
}
}
@Override
public void performPeriodicMaintenance() throws DataException {
// Defrag DB - takes a while!
try (Statement stmt = this.connection.createStatement()) {
LOGGER.info("performing maintenance - this will take a while");
stmt.execute("CHECKPOINT");
stmt.execute("CHECKPOINT DEFRAG");
LOGGER.info("maintenance completed");
} catch (SQLException e) {
throw new DataException("Unable to defrag repository");
}
}
@Override
public void exportNodeLocalData() throws DataException {
try (Statement stmt = this.connection.createStatement()) {
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE MintingAccounts DATA TO 'MintingAccounts.script'");
stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'");
LOGGER.info("Exported sensitive/node-local data: minting keys and trade bot states");
} catch (SQLException e) {
throw new DataException("Unable to export sensitive/node-local data from repository");
}
}
@Override
public void importDataFromFile(String filename) throws DataException {
try (Statement stmt = this.connection.createStatement()) {
LOGGER.info(() -> String.format("Importing data into repository from %s", filename));
String escapedFilename = stmt.enquoteLiteral(filename);
stmt.execute("PERFORM IMPORT SCRIPT DATA FROM " + escapedFilename + " STOP ON ERROR");
LOGGER.info(() -> String.format("Imported data into repository from %s", filename));
} catch (SQLException e) {
LOGGER.info(() -> String.format("Failed to import data into repository from %s: %s", filename, e.getMessage()));
throw new DataException("Unable to export sensitive/node-local data from repository: " + e.getMessage());
}
}
/** Returns DB pathname from passed connection URL. If memory DB, returns "mem". */
private static String getDbPathname(String connectionUrl) {
/*package*/ static String getDbPathname(String connectionUrl) {
Pattern pattern = Pattern.compile("hsqldb:(mem|file):(.*?)(;|$)");
Matcher matcher = pattern.matcher(connectionUrl);
@@ -370,11 +462,12 @@ public class HSQLDBRepository implements Repository {
LOGGER.info("Attempting repository recovery using backup");
// Move old repository files out the way
Files.walk(oldRepoDirPath)
.sorted(Comparator.reverseOrder())
try (Stream<Path> paths = Files.walk(oldRepoDirPath)) {
paths.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.filter(file -> file.getPath().startsWith(dbPathname))
.forEach(File::delete);
}
try (Statement stmt = connection.createStatement()) {
// Now "backup" the backup back to original repository location (the parent).
@@ -414,7 +507,33 @@ public class HSQLDBRepository implements Repository {
if (this.sqlStatements != null)
this.sqlStatements.add(sql);
return this.connection.prepareStatement(sql);
return cachePreparedStatement(sql);
}
private PreparedStatement cachePreparedStatement(String sql) throws SQLException {
/*
* We cache a duplicate PreparedStatement for this SQL string,
* which we never close, which means HSQLDB also caches a parsed,
* prepared statement that can be reused for subsequent
* calls to HSQLDB.prepareStatement(sql).
*
* See org.hsqldb.StatementManager for more details.
*/
PreparedStatement preparedStatement = this.preparedStatementCache.get(sql);
if (preparedStatement == null || preparedStatement.isClosed()) {
if (preparedStatement != null)
// This shouldn't occur, so log, but recompile
LOGGER.debug(() -> String.format("Recompiling closed PreparedStatement: %s", sql));
preparedStatement = this.connection.prepareStatement(sql);
this.preparedStatementCache.put(sql, preparedStatement);
} else {
// Clean up ready for reuse
preparedStatement.clearBatch();
preparedStatement.clearParameters();
}
return preparedStatement;
}
/**
@@ -430,9 +549,8 @@ public class HSQLDBRepository implements Repository {
public ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
PreparedStatement preparedStatement = this.prepareStatement(sql);
// Close the PreparedStatement when the ResultSet is closed otherwise there's a potential resource leak.
// We can't use try-with-resources here as closing the PreparedStatement on return would also prematurely close the ResultSet.
preparedStatement.closeOnCompletion();
// We don't close the PreparedStatement when the ResultSet is closed because we cached PreparedStatements now.
// They are cleaned up when connection/session is closed.
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
@@ -442,7 +560,7 @@ public class HSQLDBRepository implements Repository {
long queryTime = System.currentTimeMillis() - beforeQuery;
if (queryTime > this.slowQueryThreshold) {
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
logStatements();
}
@@ -460,7 +578,7 @@ public class HSQLDBRepository implements Repository {
* @param objects
* @throws SQLException
*/
private void prepareExecute(PreparedStatement preparedStatement, Object... objects) throws SQLException {
private void bindStatementParams(PreparedStatement preparedStatement, Object... objects) throws SQLException {
for (int i = 0; i < objects.length; ++i)
// Special treatment for BigDecimals so that they retain their "scale",
// which would otherwise be assumed as 0.
@@ -481,7 +599,7 @@ public class HSQLDBRepository implements Repository {
* @throws SQLException
*/
private ResultSet checkedExecuteResultSet(PreparedStatement preparedStatement, Object... objects) throws SQLException {
prepareExecute(preparedStatement, objects);
bindStatementParams(preparedStatement, objects);
if (!preparedStatement.execute())
throw new SQLException("Fetching from database produced no results");
@@ -504,31 +622,52 @@ public class HSQLDBRepository implements Repository {
* @return number of changed rows
* @throws SQLException
*/
/* package */ int checkedExecuteUpdateCount(String sql, Object... objects) throws SQLException {
try (PreparedStatement preparedStatement = this.prepareStatement(sql)) {
prepareExecute(preparedStatement, objects);
/* package */ int executeCheckedUpdate(String sql, Object... objects) throws SQLException {
return this.executeCheckedBatchUpdate(sql, Collections.singletonList(objects));
}
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
/**
* Execute batched PreparedStatement
*
* @param preparedStatement
* @param objects
* @return number of changed rows
* @throws SQLException
*/
/* package */ int executeCheckedBatchUpdate(String sql, List<Object[]> batchedObjects) throws SQLException {
// Nothing to do?
if (batchedObjects == null || batchedObjects.isEmpty())
return 0;
if (preparedStatement.execute())
throw new SQLException("Database produced results, not row count");
PreparedStatement preparedStatement = this.prepareStatement(sql);
for (Object[] objects : batchedObjects) {
this.bindStatementParams(preparedStatement, objects);
preparedStatement.addBatch();
}
if (this.slowQueryThreshold != null) {
long queryTime = System.currentTimeMillis() - beforeQuery;
long beforeQuery = this.slowQueryThreshold == null ? 0 : System.currentTimeMillis();
if (queryTime > this.slowQueryThreshold) {
LOGGER.info(() -> String.format("HSQLDB query took %d ms: %s", queryTime, sql), new SQLException("slow query"));
int[] updateCounts = preparedStatement.executeBatch();
logStatements();
}
if (this.slowQueryThreshold != null) {
long queryTime = System.currentTimeMillis() - beforeQuery;
if (queryTime > this.slowQueryThreshold) {
LOGGER.info(() -> String.format("[Session %d] HSQLDB query took %d ms: %s", this.sessionId, queryTime, sql), new SQLException("slow query"));
logStatements();
}
}
int rowCount = preparedStatement.getUpdateCount();
if (rowCount == -1)
int totalCount = 0;
for (int i = 0; i < updateCounts.length; ++i) {
if (updateCounts[i] < 0)
throw new SQLException("Database returned invalid row count");
return rowCount;
totalCount += updateCounts[i];
}
return totalCount;
}
/**
@@ -598,7 +737,25 @@ public class HSQLDBRepository implements Repository {
sql.append(" WHERE ");
sql.append(whereClause);
return this.checkedExecuteUpdateCount(sql.toString(), objects);
return this.executeCheckedUpdate(sql.toString(), objects);
}
/**
* Delete rows from database table.
*
* @param tableName
* @param whereClause
* @param objects
* @throws SQLException
*/
public int deleteBatch(String tableName, String whereClause, List<Object[]> batchedObjects) throws SQLException {
StringBuilder sql = new StringBuilder(256);
sql.append("DELETE FROM ");
sql.append(tableName);
sql.append(" WHERE ");
sql.append(whereClause);
return this.executeCheckedBatchUpdate(sql.toString(), batchedObjects);
}
/**
@@ -612,7 +769,7 @@ public class HSQLDBRepository implements Repository {
sql.append("DELETE FROM ");
sql.append(tableName);
return this.checkedExecuteUpdateCount(sql.toString());
return this.executeCheckedUpdate(sql.toString());
}
/**
@@ -690,15 +847,15 @@ public class HSQLDBRepository implements Repository {
if (this.sqlStatements == null)
return;
LOGGER.info(() -> String.format("HSQLDB SQL statements (session %d) leading up to this were:", this.sessionId));
LOGGER.info(() -> String.format("[Session %d] HSQLDB SQL statements leading up to this were:", this.sessionId));
for (String sql : this.sqlStatements)
LOGGER.info(sql);
LOGGER.info(() -> String.format("[Session %d] %s", this.sessionId, sql));
}
/** Logs other HSQLDB sessions then re-throws passed exception */
public SQLException examineException(SQLException e) throws SQLException {
LOGGER.error(String.format("HSQLDB error (session %d): %s", this.sessionId, e.getMessage()), e);
/** Logs other HSQLDB sessions then returns passed exception */
public SQLException examineException(SQLException e) {
LOGGER.error(() -> String.format("[Session %d] HSQLDB error: %s", this.sessionId, e.getMessage()), e);
logStatements();
@@ -716,7 +873,11 @@ public class HSQLDBRepository implements Repository {
String thisWaitingFor = resultSet.getString(5);
String currentStatement = resultSet.getString(6);
LOGGER.error(String.format("Session %d, %s transaction (size %d), waiting for this '%s', this waiting for '%s', current statement: %s",
// Skip logging idle sessions
if (transactionSize == 0 && waitingForThis.isEmpty() && thisWaitingFor.isEmpty() && currentStatement.isEmpty())
continue;
LOGGER.error(() -> String.format("Session %d, %s transaction (size %d), waiting for this '%s', this waiting for '%s', current statement: %s",
systemSessionId, (inTransaction ? "in" : "not in"), transactionSize, waitingForThis, thisWaitingFor, currentStatement));
} while (resultSet.next());
} catch (SQLException de) {
@@ -728,14 +889,19 @@ public class HSQLDBRepository implements Repository {
}
private void assertEmptyTransaction(String context) throws DataException {
try (Statement stmt = this.connection.createStatement()) {
String sql = "SELECT transaction, transaction_size FROM information_schema.system_sessions WHERE session_id = ?";
try {
PreparedStatement stmt = this.cachePreparedStatement(sql);
stmt.setLong(1, this.sessionId);
// Diagnostic check for uncommitted changes
if (!stmt.execute("SELECT transaction, transaction_size FROM information_schema.system_sessions WHERE session_id = " + this.sessionId)) // TRANSACTION_SIZE() broken?
if (!stmt.execute()) // TRANSACTION_SIZE() broken?
throw new DataException("Unable to check repository status after " + context);
try (ResultSet resultSet = stmt.getResultSet()) {
if (resultSet == null || !resultSet.next()) {
LOGGER.warn(String.format("Unable to check repository status after %s", context));
LOGGER.warn(() -> String.format("Unable to check repository status after %s", context));
return;
}
@@ -743,7 +909,11 @@ public class HSQLDBRepository implements Repository {
int transactionCount = resultSet.getInt(2);
if (inTransaction && transactionCount != 0) {
LOGGER.warn(String.format("Uncommitted changes (%d) after %s, session [%d]", transactionCount, context, this.sessionId), new Exception("Uncommitted repository changes"));
LOGGER.warn(() -> String.format("Uncommitted changes (%d) after %s, session [%d]",
transactionCount,
context,
this.sessionId),
new Exception("Uncommitted repository changes"));
logStatements();
}
}

View File

@@ -25,6 +25,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
private String connectionUrl;
private HSQLDBPool connectionPool;
private final boolean wasPristine;
/**
* Constructs new RepositoryFactory using passed <tt>connectionUrl</tt>.
@@ -65,12 +66,17 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
// Perform DB updates?
try (final Connection connection = this.connectionPool.getConnection()) {
HSQLDBDatabaseUpdates.updateDatabase(connection);
this.wasPristine = HSQLDBDatabaseUpdates.updateDatabase(connection);
} catch (SQLException e) {
throw new DataException("Repository initialization error", e);
}
}
@Override
public boolean wasPristineAtOpen() {
return this.wasPristine;
}
@Override
public RepositoryFactory reopen() throws DataException {
return new HSQLDBRepositoryFactory(this.connectionUrl);

View File

@@ -60,7 +60,9 @@ public class HSQLDBSaver {
*/
public boolean execute(HSQLDBRepository repository) throws SQLException {
String sql = this.formatInsertWithPlaceholders();
try (PreparedStatement preparedStatement = repository.prepareStatement(sql)) {
try {
PreparedStatement preparedStatement = repository.prepareStatement(sql);
this.bindValues(preparedStatement);
return preparedStatement.execute();

View File

@@ -19,7 +19,6 @@ import org.qortal.data.PaymentData;
import org.qortal.data.group.GroupApprovalData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.GroupApprovalTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.TransferAssetTransactionData;
import org.qortal.repository.DataException;
@@ -586,6 +585,69 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
Integer minBlockHeight, Integer maxBlockHeight) throws DataException {
List<byte[]> signatures = new ArrayList<>();
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT signature FROM Transactions ");
List<String> whereClauses = new ArrayList<>();
List<Object> bindParams = new ArrayList<>();
if (txType != null) {
whereClauses.add("type = ?");
bindParams.add(txType.value);
}
if (publicKey != null) {
whereClauses.add("creator = ?");
bindParams.add(publicKey);
}
if (minBlockHeight != null) {
whereClauses.add("Transactions.block_height >= ?");
bindParams.add(minBlockHeight);
}
if (maxBlockHeight != null) {
whereClauses.add("Transactions.block_height <= ?");
bindParams.add(maxBlockHeight);
}
if (!whereClauses.isEmpty()) {
sql.append(" WHERE ");
final int whereClausesSize = whereClauses.size();
for (int wci = 0; wci < whereClausesSize; ++wci) {
if (wci != 0)
sql.append(" AND ");
sql.append(whereClauses.get(wci));
}
}
sql.append(" ORDER BY Transactions.created_when");
LOGGER.trace(() -> String.format("Transaction search SQL: %s", sql));
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return signatures;
do {
byte[] signature = resultSet.getBytes(1);
signatures.add(signature);
} while (resultSet.next());
return signatures;
} catch (SQLException e) {
throw new DataException("Unable to fetch matching transaction signatures from repository", e);
}
}
@Override
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException {
StringBuilder sql = new StringBuilder(1024);
@@ -631,43 +693,6 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT signature from MessageTransactions "
+ "JOIN Transactions USING (signature) "
+ "JOIN BlockTransactions ON transaction_signature = signature "
+ "WHERE recipient = ?");
sql.append("ORDER BY Transactions.created_when");
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), recipient)) {
if (resultSet == null)
return messageTransactionsData;
do {
byte[] signature = resultSet.getBytes(1);
TransactionData transactionData = this.fromSignature(signature);
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
return null;
messageTransactionsData.add((MessageTransactionData) transactionData);
} while (resultSet.next());
return messageTransactionsData;
} catch (SQLException e) {
throw new DataException("Unable to fetch trade-bot messages from repository", e);
}
}
@Override
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
throws DataException {

View File

@@ -5,6 +5,7 @@ import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Locale;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
@@ -41,6 +42,9 @@ public class Settings {
// Settings, and other config files
private String userPath;
// General
private String localeLang = Locale.getDefault().getLanguage();
// Common to all networking (API/P2P)
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
@@ -61,6 +65,7 @@ public class Settings {
"::1", "127.0.0.1"
};
private Boolean apiRestricted;
private String apiKey = null;
private boolean apiLoggingEnabled = false;
private boolean apiDocumentationEnabled = false;
// Both of these need to be set for API to use SSL
@@ -79,6 +84,26 @@ public class Settings {
private long repositoryBackupInterval = 0; // ms
/** Whether to show a notification when we backup repository. */
private boolean showBackupNotification = false;
/** How long between repository checkpoints (ms). */
private long repositoryCheckpointInterval = 60 * 60 * 1000L; // 1 hour (ms) default
/** Whether to show a notification when we perform repository 'checkpoint'. */
private boolean showCheckpointNotification = false;
/** How long to keep old, full, AT state data (ms). */
private long atStatesMaxLifetime = 2 * 7 * 24 * 60 * 60 * 1000L; // milliseconds
/** How often to attempt AT state trimming (ms). */
private long atStatesTrimInterval = 5678L; // milliseconds
/** Block height range to scan for trimmable AT states.<br>
* This has a significant effect on execution time. */
private int atStatesTrimBatchSize = 100; // blocks
/** Max number of AT states to trim in one go. */
private int atStatesTrimLimit = 4000; // records
/** How often to attempt online accounts signatures trimming (ms). */
private long onlineSignaturesTrimInterval = 9876L; // milliseconds
/** Block height range to scan for trimmable online accounts signatures.<br>
* This has a significant effect on execution time. */
private int onlineSignaturesTrimBatchSize = 100; // blocks
// Peer-to-peer related
private boolean isTestNet = false;
@@ -98,6 +123,9 @@ public class Settings {
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
// Also crosschain-related:
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
private boolean tradebotSystrayEnabled = false;
// Repository related
/** Queries that take longer than this are logged. (milliseconds) */
@@ -248,6 +276,9 @@ public class Settings {
// Validation goes here
if (this.minBlockchainPeers < 1)
throwValidationError("minBlockchainPeers must be at least 1");
if (this.apiKey != null && this.apiKey.trim().length() < 8)
throwValidationError("apiKey must be at least 8 characters");
}
// Getters / setters
@@ -256,6 +287,10 @@ public class Settings {
return this.userPath;
}
public String getLocaleLang() {
return this.localeLang;
}
public int getUiServerPort() {
return this.uiPort;
}
@@ -292,6 +327,10 @@ public class Settings {
return !BlockChain.getInstance().isTestChain();
}
public String getApiKey() {
return this.apiKey;
}
public boolean isApiLoggingEnabled() {
return this.apiLoggingEnabled;
}
@@ -367,6 +406,10 @@ public class Settings {
return this.bitcoinNet;
}
public boolean isTradebotSystrayEnabled() {
return this.tradebotSystrayEnabled;
}
public Long getSlowQueryThreshold() {
return this.slowQueryThreshold;
}
@@ -399,4 +442,36 @@ public class Settings {
return this.showBackupNotification;
}
public long getRepositoryCheckpointInterval() {
return this.repositoryCheckpointInterval;
}
public boolean getShowCheckpointNotification() {
return this.showCheckpointNotification;
}
public long getAtStatesMaxLifetime() {
return this.atStatesMaxLifetime;
}
public long getAtStatesTrimInterval() {
return this.atStatesTrimInterval;
}
public int getAtStatesTrimBatchSize() {
return this.atStatesTrimBatchSize;
}
public int getAtStatesTrimLimit() {
return this.atStatesTrimLimit;
}
public long getOnlineSignaturesTrimInterval() {
return this.onlineSignaturesTrimInterval;
}
public int getOnlineSignaturesTrimBatchSize() {
return this.onlineSignaturesTrimBatchSize;
}
}

View File

@@ -28,7 +28,8 @@ public class NTP implements Runnable {
private static volatile boolean isStopping = false;
private static ExecutorService instanceExecutor;
private static NTP instance;
private static volatile Long offset = null;
private static volatile boolean isOffsetSet = false;
private static volatile long offset = 0;
static class NTPServer {
private static final int MIN_POLL = 64;
@@ -136,6 +137,7 @@ public class NTP implements Runnable {
public static synchronized void setFixedOffset(Long offset) {
// Fix offset, e.g. for testing
NTP.offset = offset;
isOffsetSet = true;
}
/**
@@ -144,7 +146,7 @@ public class NTP implements Runnable {
* @return internet time (ms), or null if unsynchronized.
*/
public static Long getTime() {
if (NTP.offset == null)
if (!isOffsetSet)
return null;
return System.currentTimeMillis() + NTP.offset;
@@ -248,6 +250,7 @@ public class NTP implements Runnable {
thresholdStddev, filteredMean, filteredStddev, numberValues, ntpServers.size()));
NTP.offset = (long) filteredMean;
isOffsetSet = true;
LOGGER.debug(() -> String.format("New NTP offset: %d", NTP.offset));
}
}

View File

@@ -8,6 +8,12 @@ BLOCKCHAIN_NEEDS_SYNC = blockchain needs to synchronize first
# Blocks
BLOCK_UNKNOWN = block unknown
BTC_BALANCE_ISSUE = insufficient Bitcoin balance
BTC_NETWORK_ISSUE = Bitcoin/ElectrumX network issue
BTC_TOO_SOON = too soon to broadcast Bitcoin transaction (lockTime/median block time)
CANNOT_MINT = account cannot mint
GROUP_UNKNOWN = group unknown
@@ -15,7 +21,7 @@ GROUP_UNKNOWN = group unknown
INVALID_ADDRESS = invalid address
# Assets
INVALID_ASSET_ID = invalid asset id
INVALID_ASSET_ID = invalid asset ID
INVALID_CRITERIA = invalid search criteria
@@ -36,18 +42,21 @@ INVALID_REFERENCE = invalid reference
# Validation
INVALID_SIGNATURE = invalid signature
JSON = failed to parse json message
JSON = failed to parse JSON message
NAME_UNKNOWN = name unknown
NON_PRODUCTION = this API call is not permitted for production systems
NO_TIME_SYNC = no clock synchronization yet
ORDER_UNKNOWN = unknown asset order ID
PUBLIC_KEY_NOT_FOUND = public key not found
REPOSITORY_ISSUE = repository error
# This one is special in that caller expected to pass two additional strings, hence the two %s
TRANSACTION_INVALID = transaction invalid: %s (%s)
TRANSACTION_UNKNOWN = transaction unknown

View File

@@ -0,0 +1,53 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# Keys are from api.ApiError enum
ADDRESS_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0083\u00D1\u0087\u00D0\u00B5\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
# Blocks
BLOCK_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA
CANNOT_MINT = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082\u00D1\u008C
GROUP_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
# Assets
INVALID_ASSET_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
INVALID_CRITERIA = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00BA\u00D1\u0080\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D1\u0080\u00D0\u00B8\u00D0\u00B8 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00B0
INVALID_DATA = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00BD\u00D1\u008B\u00D0\u00B5
INVALID_HEIGHT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B2\u00D1\u008B\u00D1\u0081\u00D0\u00BE\u00D1\u0082\u00D0\u00B0 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D0\u00B0
INVALID_NETWORK_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00B5\u00D1\u0082\u00D0\u00B5\u00D0\u00B2\u00D0\u00BE\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
INVALID_ORDER_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
INVALID_PRIVATE_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
# Validation
INVALID_SIGNATURE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D1\u008C
JSON = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D0\u00B1\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 json
NAME_UNKNOWN = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00BE
ORDER_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
PUBLIC_KEY_NOT_FOUND = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B9\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD
REPOSITORY_ISSUE = \u00D0\u00BE\u00D1\u0088\u00D0\u00B8\u00D0\u00B1\u00D0\u00BA\u00D0\u00B0 \u00D1\u0080\u00D0\u00B5\u00D0\u00BF\u00D0\u00BE\u00D0\u00B7\u00D0\u00B8\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D1\u008F
TRANSACTION_INVALID = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0: %s (%s)
TRANSACTION_UNKNOWN = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0
TRANSFORMATION_ERROR = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0083\u00D0\u00B4\u00D0\u00B0\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D1\u008C \u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C JSON \u00D0\u00B2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008E
UNAUTHORIZED = \u00D0\u00B2\u00D1\u008B\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2 API \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B2\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B8\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD

View File

@@ -19,6 +19,8 @@ CREATING_BACKUP_OF_DB_FILES = Creating backup of database files...
DB_BACKUP = Database Backup
DB_CHECKPOINT = Database Checkpoint
EXIT = Exit
MINTING_DISABLED = NOT minting
@@ -34,6 +36,8 @@ NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix.
OPEN_UI = Open UI
PERFORMING_DB_CHECKPOINT = Saving uncommitted database changes...
SYNCHRONIZE_CLOCK = Synchronize clock
SYNCHRONIZING_BLOCKCHAIN = Synchronizing

View File

@@ -0,0 +1,29 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
APPLYING_UPDATE_AND_RESTARTING = \u00D0\u009F\u00D1\u0080\u00D0\u00B8\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B2\u00D1\u0082\u00D0\u00BE\u00D0\u00BC\u00D0\u00B0\u00D1\u0082\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D0\u00BA\u00D0\u00BE\u00D0\u00B3\u00D0\u00BE \u00D0\u00BE\u00D0\u00B1\u00D0\u00BD\u00D0\u00BE\u00D0\u00B2\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00B8 \u00D0\u00BF\u00D0\u00B5\u00D1\u0080\u00D0\u00B5\u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D0\u00BA\u00D0\u00B0...
AUTO_UPDATE = \u00D0\u0090\u00D0\u00B2\u00D1\u0082\u00D0\u00BE\u00D0\u00BC\u00D0\u00B0\u00D1\u0082\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D0\u00BA\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BE\u00D0\u00B1\u00D0\u00BD\u00D0\u00BE\u00D0\u00B2\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5
BLOCK_HEIGHT = \u00D0\u0092\u00D1\u008B\u00D1\u0081\u00D0\u00BE\u00D1\u0082\u00D0\u00B0 \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D0\u00B0
CHECK_TIME_ACCURACY = \u00D0\u009F\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BA\u00D0\u00B0 \u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00BE\u00D0\u00B3\u00D0\u00BE \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
CONNECTING = \u00D0\u009F\u00D0\u00BE\u00D0\u00B4\u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5
CONNECTION = \u00D0\u00A1\u00D0\u00BE\u00D0\u00B5\u00D0\u00B4\u00D0\u00B8\u00D0\u00BD\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5
CONNECTIONS = \u00D0\u00A1\u00D0\u00BE\u00D0\u00B5\u00D0\u00B4\u00D0\u00B8\u00D0\u00BD\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B9
MINTING_DISABLED = \u00D0\u00A7\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00BA\u00D0\u00B0 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
MINTING_ENABLED = \u00D0\u00A7\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00BD\u00D0\u00B0
# Nagging about lack of NTP time sync
NTP_NAG_CAPTION = \u00D0\u00A7\u00D0\u00B0\u00D1\u0081\u00D1\u008B \u00D0\u00BA\u00D0\u00BE\u00D0\u00BC\u00D0\u00BF\u00D1\u008C\u00D1\u008E\u00D1\u0082\u00D0\u00B5\u00D1\u0080\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D1\u008B!
NTP_NAG_TEXT_UNIX = \u00D0\u00A3\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D0\u00BD\u00D0\u00BE\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5 \u00D1\u0081\u00D0\u00BB\u00D1\u0083\u00D0\u00B6\u00D0\u00B1\u00D1\u0083 NTP, \u00D1\u0087\u00D1\u0082\u00D0\u00BE\u00D0\u00B1\u00D1\u008B \u00D0\u00BF\u00D0\u00BE\u00D0\u00BB\u00D1\u0083\u00D1\u0087\u00D0\u00B8\u00D1\u0082\u00D1\u008C \u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D1\u008F
OPEN_UI = \u00D0\u009E\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008C \u00D0\u00BF\u00D0\u00BE\u00D0\u00BB\u00D1\u008C\u00D0\u00B7\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D1\u0081\u00D0\u00BA\u00D0\u00B8\u00D0\u00B9 \u00D0\u00B8\u00D0\u00BD\u00D1\u0082\u00D0\u00B5\u00D1\u0080\u00D1\u0084\u00D0\u00B5\u00D0\u00B9\u00D1\u0081
SYNCHRONIZING_CLOCK = \u00D0\u009F\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8

View File

@@ -11,166 +11,174 @@ ALREADY_VOTED_FOR_THAT_OPTION = already voted for that option
ASSET_ALREADY_EXISTS = asset already exists
ASSET_DOES_NOT_EXIST = ASSET_DOES_NOT_EXIST
ASSET_DOES_NOT_EXIST = asset does not exist
ASSET_DOES_NOT_MATCH_AT = ASSET_DOES_NOT_MATCH_AT
ASSET_DOES_NOT_MATCH_AT = asset does not match AT's asset
ASSET_NOT_SPENDABLE = ASSET_NOT_SPENDABLE
ASSET_NOT_SPENDABLE = asset is not spendable
AT_ALREADY_EXISTS = AT_ALREADY_EXISTS
AT_ALREADY_EXISTS = AT already exists
AT_IS_FINISHED = AT_IS_FINISHED
AT_IS_FINISHED = AT has finished
AT_UNKNOWN = AT_UNKNOWN
AT_UNKNOWN = AT unknown
BANNED_FROM_GROUP = BANNED_FROM_GROUP
BANNED_FROM_GROUP = banned from group
BAN_EXISTS = BAN_EXISTS
BAN_EXISTS = ban already exists
BAN_UNKNOWN = BAN_UNKNOWN
BAN_UNKNOWN = ban unknown
BUYER_ALREADY_OWNER = BUYER_ALREADY_OWNER
BUYER_ALREADY_OWNER = buyer is already owner
CLOCK_NOT_SYNCED = CLOCK_NOT_SYNCED
CHAT = CHAT transactions are never valid for inclusion into blocks
DUPLICATE_OPTION = DUPLICATE_OPTION
CLOCK_NOT_SYNCED = clock not synchronized
GROUP_ALREADY_EXISTS = GROUP_ALREADY_EXISTS
DUPLICATE_OPTION = duplicate option
GROUP_APPROVAL_DECIDED = GROUP_APPROVAL_DECIDED
GROUP_ALREADY_EXISTS = group already exists
GROUP_APPROVAL_NOT_REQUIRED = GROUP_APPROVAL_NOT_REQUIRED
GROUP_APPROVAL_DECIDED = group-approval already decided
GROUP_DOES_NOT_EXIST = GROUP_DOES_NOT_EXIST
GROUP_APPROVAL_NOT_REQUIRED = group-approval not required
GROUP_ID_MISMATCH = GROUP_ID_MISMATCH
GROUP_DOES_NOT_EXIST = group does not exist
GROUP_OWNER_CANNOT_LEAVE = GROUP_OWNER_CANNOT_LEAVE
GROUP_ID_MISMATCH = group ID mismatch
HAVE_EQUALS_WANT = HAVE_EQUALS_WANT
GROUP_OWNER_CANNOT_LEAVE = group owner cannot leave group
INSUFFICIENT_FEE = INSUFFICIENT_FEE
HAVE_EQUALS_WANT = have-asset is the same as want-asset
INVALID_ADDRESS = INVALID_ADDRESS
INCORRECT_NONCE = incorrect PoW nonce
INVALID_AMOUNT = INVALID_AMOUNT
INSUFFICIENT_FEE = insufficient fee
INVALID_ASSET_OWNER = INVALID_ASSET_OWNER
INVALID_ADDRESS = invalid address
INVALID_AT_TRANSACTION = INVALID_AT_TRANSACTION
INVALID_AMOUNT = invalid amount
INVALID_AT_TYPE_LENGTH = INVALID_AT_TYPE_LENGTH
INVALID_ASSET_OWNER = invalid asset owner
INVALID_CREATION_BYTES = INVALID_CREATION_BYTES
INVALID_AT_TRANSACTION = invalid AT transaction
INVALID_DATA_LENGTH = INVALID_DATA_LENGTH
INVALID_AT_TYPE_LENGTH = invalid AT 'type' length
INVALID_DESCRIPTION_LENGTH = INVALID_DESCRIPTION_LENGTH
INVALID_CREATION_BYTES = invalid creation bytes
INVALID_GROUP_APPROVAL_THRESHOLD = INVALID_GROUP_APPROVAL_THRESHOLD
INVALID_DATA_LENGTH = invalid data length
INVALID_GROUP_ID = INVALID_GROUP_ID
INVALID_DESCRIPTION_LENGTH = invalid description length
INVALID_GROUP_OWNER = INVALID_GROUP_OWNER
INVALID_GROUP_APPROVAL_THRESHOLD = invalid group-approval threshold
INVALID_LIFETIME = INVALID_LIFETIME
INVALID_GROUP_BLOCK_DELAY = invalid group-approval block delay
INVALID_NAME_LENGTH = INVALID_NAME_LENGTH
INVALID_GROUP_ID = invalid group ID
INVALID_NAME_OWNER = INVALID_NAME_OWNER
INVALID_GROUP_OWNER = invalid group owner
INVALID_OPTIONS_COUNT = INVALID_OPTIONS_COUNT
INVALID_LIFETIME = invalid lifetime
INVALID_OPTION_LENGTH = INVALID_OPTION_LENGTH
INVALID_NAME_LENGTH = invalid name length
INVALID_ORDER_CREATOR = INVALID_ORDER_CREATOR
INVALID_NAME_OWNER = invalid name owner
INVALID_PAYMENTS_COUNT = INVALID_PAYMENTS_COUNT
INVALID_OPTIONS_COUNT = invalid options count
INVALID_PUBLIC_KEY = INVALID_PUBLIC_KEY
INVALID_OPTION_LENGTH = invalid options length
INVALID_QUANTITY = INVALID_QUANTITY
INVALID_ORDER_CREATOR = invalid order creator
INVALID_REFERENCE = INVALID_REFERENCE
INVALID_PAYMENTS_COUNT = invalid payments count
INVALID_RETURN = INVALID_RETURN
INVALID_PUBLIC_KEY = invalid public key
INVALID_REWARD_SHARE_PERCENT = INVALID_REWARD_SHARE_PERCENT
INVALID_QUANTITY = invalid quantity
INVALID_SELLER = INVALID_SELLER
INVALID_REFERENCE = invalid reference
INVALID_TAGS_LENGTH = INVALID_TAGS_LENGTH
INVALID_RETURN = invalid return
INVALID_TX_GROUP_ID = INVALID_TX_GROUP_ID
INVALID_REWARD_SHARE_PERCENT = invalid reward-share percent
INVALID_VALUE_LENGTH = INVALID_VALUE_LENGTH
INVALID_SELLER = invalid seller
INVITE_UNKNOWN = INVITE_UNKNOWN
INVALID_TAGS_LENGTH = invalid 'tags' length
JOIN_REQUEST_EXISTS = JOIN_REQUEST_EXISTS
INVALID_TX_GROUP_ID = invalid transaction group ID
MAXIMUM_REWARD_SHARES = MAXIMUM_REWARD_SHARES
INVALID_VALUE_LENGTH = invalid 'value' length
MISSING_CREATOR = MISSING_CREATOR
INVITE_UNKNOWN = group invite unknown
MULTIPLE_NAMES_FORBIDDEN = MULTIPLE_NAMES_FORBIDDEN
JOIN_REQUEST_EXISTS = group join request already exists
NAME_ALREADY_FOR_SALE = NAME_ALREADY_FOR_SALE
MAXIMUM_REWARD_SHARES = already at maximum number of reward-shares for this account
NAME_ALREADY_REGISTERED = NAME_ALREADY_REGISTERED
MISSING_CREATOR = missing creator
NAME_DOES_NOT_EXIST = NAME_DOES_NOT_EXIST
MULTIPLE_NAMES_FORBIDDEN = multiple registered names per account is forbidden
NAME_NOT_FOR_SALE = NAME_NOT_FOR_SALE
NAME_ALREADY_FOR_SALE = name already for sale
NAME_NOT_LOWER_CASE = NAME_NOT_LOWER_CASE
NAME_ALREADY_REGISTERED = name already registered
NEGATIVE_AMOUNT = NEGATIVE_AMOUNT
NAME_DOES_NOT_EXIST = name does not exist
NEGATIVE_FEE = NEGATIVE_FEE
NAME_NOT_FOR_SALE = name is not for sale
NEGATIVE_PRICE = NEGATIVE_PRICE
NAME_NOT_NORMALIZED = name not in Unicode 'normalized' form
NOT_GROUP_ADMIN = NOT_GROUP_ADMIN
NEGATIVE_AMOUNT = invalid/negative amount
NOT_GROUP_MEMBER = NOT_GROUP_MEMBER
NEGATIVE_FEE = invalid/negative fee
NOT_MINTING_ACCOUNT = NOT_MINTING_ACCOUNT
NEGATIVE_PRICE = invalid/negative price
NOT_YET_RELEASED = NOT_YET_RELEASED
NOT_GROUP_ADMIN = account is not a group admin
NO_BALANCE = NO_BALANCE
NOT_GROUP_MEMBER = account is not a group member
NOT_MINTING_ACCOUNT = account cannot mint
NOT_YET_RELEASED = feature not yet released
NO_BALANCE = insufficient balance
NO_BLOCKCHAIN_LOCK = node's blockchain currently busy
NO_FLAG_PERMISSION = NO_FLAG_PERMISSION
NO_FLAG_PERMISSION = account does not have that permission
OK = OK
ORDER_ALREADY_CLOSED = ORDER_ALREADY_CLOSED
ORDER_ALREADY_CLOSED = asset trade order is already closed
ORDER_DOES_NOT_EXIST = ORDER_DOES_NOT_EXIST
ORDER_DOES_NOT_EXIST = asset trade order does not exist
POLL_ALREADY_EXISTS = POLL_ALREADY_EXISTS
POLL_ALREADY_EXISTS = poll already exists
POLL_DOES_NOT_EXIST = POLL_DOES_NOT_EXIST
POLL_DOES_NOT_EXIST = poll does not exist
POLL_OPTION_DOES_NOT_EXIST = POLL_OPTION_DOES_NOT_EXIST
POLL_OPTION_DOES_NOT_EXIST = poll option does not exist
PUBLIC_KEY_UNKNOWN = PUBLIC_KEY_UNKNOWN
PUBLIC_KEY_UNKNOWN = public key unknown
SELF_SHARE_EXISTS = SELF_SHARE_EXISTS
REWARD_SHARE_UNKNOWN = reward-share unknown
TIMESTAMP_TOO_NEW = TIMESTAMP_TOO_NEW
SELF_SHARE_EXISTS = self-share (reward-share) already exists
TIMESTAMP_TOO_OLD = TIMESTAMP_TOO_OLD
TIMESTAMP_TOO_NEW = timestamp too new
TOO_MANY_UNCONFIRMED = TOO_MANY_UNCONFIRMED
TIMESTAMP_TOO_OLD = timestamp too old
TRANSACTION_ALREADY_CONFIRMED = TRANSACTION_ALREADY_CONFIRMED
TOO_MANY_UNCONFIRMED = account has too many unconfirmed transactions pending
TRANSACTION_ALREADY_EXISTS = TRANSACTION_ALREADY_EXISTS
TRANSACTION_ALREADY_CONFIRMED = transaction has already confirmed
TRANSACTION_UNKNOWN = TRANSACTION_UNKNOWN
TRANSACTION_ALREADY_EXISTS = transaction already exists
TX_GROUP_ID_MISMATCH = TX_GROUP_ID_MISMATCH
TRANSACTION_UNKNOWN = transaction unknown
TX_GROUP_ID_MISMATCH = transaction's group ID does not match

View File

@@ -0,0 +1,164 @@
ACCOUNT_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
ACCOUNT_CANNOT_REWARD_SHARE = \u00D0\u00B0\u00D0\u00BA\u00D0\u00BA\u00D0\u00B0\u00D1\u0083\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5\u00D0\u00BC
ALREADY_GROUP_ADMIN = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
ALREADY_GROUP_MEMBER = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
ALREADY_VOTED_FOR_THAT_OPTION = \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B3\u00D0\u00BE\u00D0\u00BB\u00D0\u00BE\u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BB\u00D0\u00B8 \u00D0\u00B7\u00D0\u00B0 \u00D1\u008D\u00D1\u0082\u00D0\u00BE\u00D1\u0082 \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
ASSET_ALREADY_EXISTS = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
ASSET_DOES_NOT_EXIST = \u00D0\u0090\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
ASSET_DOES_NOT_MATCH_AT = \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B2\u00D0\u00BF\u00D0\u00B0\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082 \u00D1\u0081 \u00D0\u0090\u00D0\u00A2
AT_ALREADY_EXISTS = AT \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
AT_IS_FINISHED = AT \u00D0\u00B2 \u00D0\u00B7\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B8
AT_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u0090\u00D0\u00A2
BANNED_FROM_GROUP = \u00D0\u00B8\u00D1\u0081\u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087\u00D0\u00B5\u00D0\u00BD \u00D0\u00B8\u00D0\u00B7 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
BAN_EXISTS = \u00D0\u0091\u00D0\u00B0\u00D0\u00BD
BAN_UNKNOWN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BD
BUYER_ALREADY_OWNER = \u00D0\u00BF\u00D0\u00BE\u00D0\u00BA\u00D1\u0083\u00D0\u00BF\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00B1\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D0\u00BD\u00D0\u00BD\u00D0\u00B8\u00D0\u00BA
DUPLICATE_OPTION = \u00D0\u00B4\u00D1\u0083\u00D0\u00B1\u00D0\u00BB\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082
GROUP_ALREADY_EXISTS = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
GROUP_APPROVAL_DECIDED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
GROUP_APPROVAL_NOT_REQUIRED = \u00D0\u00B3\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00BE\u00D0\u00B2\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BE\u00D0\u00B4\u00D0\u00BE\u00D0\u00B1\u00D1\u0080\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0082\u00D1\u0080\u00D0\u00B5\u00D0\u00B1\u00D1\u0083\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
GROUP_DOES_NOT_EXIST = \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
GROUP_ID_MISMATCH = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D0\u00BE\u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
GROUP_OWNER_CANNOT_LEAVE = \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BC\u00D0\u00BE\u00D0\u00B6\u00D0\u00B5\u00D1\u0082 \u00D1\u0083\u00D0\u00B9\u00D1\u0082\u00D0\u00B8
HAVE_EQUALS_WANT = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D0\u00B5\u00D1\u008E\u00D1\u0082\u00D1\u0081\u00D1\u008F \u00D1\u0080\u00D0\u00B0\u00D0\u00B2\u00D0\u00BD\u00D1\u008B\u00D0\u00B5 \u00D0\u00B6\u00D0\u00B5\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
INSUFFICIENT_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B0
INVALID_ADDRESS = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B0\u00D0\u00B4\u00D1\u0080\u00D0\u00B5\u00D1\u0081
INVALID_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
INVALID_ASSET_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B0\u00D0\u00BA\u00D1\u0082\u00D0\u00B8\u00D0\u00B2\u00D0\u00B0
INVALID_AT_TRANSACTION = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u0090\u00D0\u00A2 \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F
INVALID_AT_TYPE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE \u00D0\u00B4\u00D0\u00BB\u00D1\u008F \u00D1\u0082\u00D0\u00B8\u00D0\u00BF\u00D0\u00B0 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B AT
INVALID_CREATION_BYTES = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B5 \u00D0\u00B1\u00D0\u00B0\u00D0\u00B9\u00D1\u0082\u00D1\u008B \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
INVALID_DESCRIPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D0\u00B8\u00D1\u0081\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
INVALID_GROUP_APPROVAL_THRESHOLD = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D1\u0080\u00D0\u00BE\u00D0\u00B3 \u00D1\u0083\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
INVALID_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
INVALID_GROUP_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B5\u00D1\u0086 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
INVALID_LIFETIME = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083 \u00D1\u0081\u00D1\u0080\u00D0\u00BE\u00D0\u00BA \u00D1\u0081\u00D0\u00BB\u00D1\u0083\u00D0\u00B6\u00D0\u00B1\u00D1\u008B
INVALID_NAME_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
INVALID_NAME_OWNER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00B2\u00D0\u00BB\u00D0\u00B0\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D1\u0086\u00D0\u00B0
INVALID_OPTIONS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B9
INVALID_OPTION_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D0\u00BE\u00D0\u00BF\u00D1\u0086\u00D0\u00B8\u00D0\u00B8
INVALID_ORDER_CREATOR = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0
INVALID_PAYMENTS_COUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BF\u00D0\u00BB\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00B6\u00D0\u00B5\u00D0\u00B9
INVALID_PUBLIC_KEY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087
INVALID_QUANTITY = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D0\u00B8\u00D1\u0087\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00BE
INVALID_REFERENCE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0081\u00D1\u008B\u00D0\u00BB\u00D0\u00BA\u00D0\u00B0
INVALID_RETURN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D1\u008B\u00D0\u00B9 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00B2\u00D1\u0080\u00D0\u00B0\u00D1\u0082
INVALID_REWARD_SHARE_PERCENT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0086\u00D0\u00B5\u00D0\u00BD\u00D1\u0082 \u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F
INVALID_SELLER = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B2\u00D0\u00B5\u00D1\u0086
INVALID_TAGS_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D0\u00B0 \u00D1\u0082\u00D1\u008D\u00D0\u00B3\u00D0\u00BE\u00D0\u00B2
INVALID_TX_GROUP_ID = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00B5\u00D0\u00B9\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D0\u00B8\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D0\u00B8\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D1\u0082\u00D0\u00B8\u00D1\u0084\u00D0\u00B8\u00D0\u00BA\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B \u00D0\u00BF\u00D0\u00B5\u00D1\u0080\u00D0\u00B5\u00D0\u00B4\u00D0\u00B0\u00D1\u0087\u00D0\u00B8
INVALID_VALUE_LENGTH = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D0\u00BF\u00D1\u0083\u00D1\u0081\u00D1\u0082\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D1\u0087\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BB\u00D0\u00B8\u00D0\u00BD\u00D1\u008B
JOIN_REQUEST_EXISTS = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D0\u00BD\u00D0\u00B0 \u00D0\u00BF\u00D1\u0080\u00D0\u00B8\u00D1\u0081\u00D0\u00BE\u00D0\u00B5\u00D0\u00B4\u00D0\u00B8\u00D0\u00BD\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
MAXIMUM_REWARD_SHARES = \u00D0\u00BC\u00D0\u00B0\u00D0\u00BA\u00D1\u0081\u00D0\u00B8\u00D0\u00BC\u00D0\u00B0\u00D0\u00BB\u00D1\u008C\u00D0\u00BD\u00D0\u00BE\u00D0\u00B5 \u00D0\u00B2\u00D0\u00BE\u00D0\u00B7\u00D0\u00BD\u00D0\u00B0\u00D0\u00B3\u00D1\u0080\u00D0\u00B0\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D0\u00B5
MISSING_CREATOR = \u00D0\u00BE\u00D1\u0082\u00D1\u0081\u00D1\u0083\u00D1\u0082\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D1\u008E\u00D1\u0089\u00D0\u00B8\u00D0\u00B9 \u00D1\u0081\u00D0\u00BE\u00D0\u00B7\u00D0\u00B4\u00D0\u00B0\u00D1\u0082\u00D0\u00B5\u00D0\u00BB\u00D1\u008C
MULTIPLE_NAMES_FORBIDDEN = \u00D0\u00BD\u00D0\u00B5\u00D1\u0081\u00D0\u00BA\u00D0\u00BE\u00D0\u00BB\u00D1\u008C\u00D0\u00BA\u00D0\u00BE \u00D0\u00B8\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD \u00D0\u00B7\u00D0\u00B0\u00D0\u00BF\u00D1\u0080\u00D0\u00B5\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
NAME_ALREADY_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B2 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B6\u00D0\u00B5
NAME_ALREADY_REGISTERED = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00B7\u00D0\u00B0\u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B8\u00D1\u0080\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D0\u00BD\u00D0\u00BE
NAME_DOES_NOT_EXIST = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
NAME_NOT_FOR_SALE = \u00D0\u00B8\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D0\u00B4\u00D0\u00B0\u00D0\u00B5\u00D1\u0082\u00D1\u0081\u00D1\u008F
NAME_NOT_LOWER_CASE = \u00D0\u00B8\u00D0\u00BC\u00D0\u00BC\u00D1\u008F \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B6\u00D0\u00BD\u00D0\u00BE \u00D1\u0081\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B0\u00D1\u0082\u00D1\u008C \u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D1\u008B\u00D0\u00B9 \u00D1\u0080\u00D0\u00B5\u00D0\u00B3\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080
NEGATIVE_AMOUNT = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D0\u00BC\u00D0\u00BC\u00D0\u00B0
NEGATIVE_FEE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D0\u00BA\u00D0\u00BE\u00D0\u00BC\u00D0\u00B8\u00D1\u0081\u00D1\u0081\u00D0\u00B8\u00D1\u008F
NEGATIVE_PRICE = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B4\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0087\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D0\u00B8\u00D0\u00BC\u00D0\u00BE\u00D1\u0081\u00D1\u0082\u00D1\u008C
NOT_GROUP_ADMIN = \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B0\u00D0\u00B4\u00D0\u00BC\u00D0\u00B8\u00D0\u00BD\u00D0\u00B8\u00D1\u0081\u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D1\u0082\u00D0\u00BE\u00D1\u0080 \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
NOT_GROUP_MEMBER = \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00BB\u00D0\u00B5\u00D0\u00BD \u00D0\u00B3\u00D1\u0080\u00D1\u0083\u00D0\u00BF\u00D0\u00BF\u00D1\u008B
NOT_MINTING_ACCOUNT = \u00D1\u0081\u00D1\u0087\u00D0\u00B5\u00D1\u0082 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0087\u00D0\u00B5\u00D0\u00BA\u00D0\u00B0\u00D0\u00BD\u00D0\u00B8\u00D1\u0082
NOT_YET_RELEASED = \u00D0\u00B5\u00D1\u0089\u00D0\u00B5 \u00D0\u00BD\u00D0\u00B5 \u00D0\u00B2\u00D1\u008B\u00D0\u00BF\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D0\u00BD\u00D0\u00BE
NO_BALANCE = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D0\u00B1\u00D0\u00B0\u00D0\u00BB\u00D0\u00B0\u00D0\u00BD\u00D1\u0081\u00D0\u00B0
NO_BLOCKCHAIN_LOCK = \u00D0\u00B1\u00D0\u00BB\u00D0\u00BE\u00D0\u00BA\u00D1\u0087\u00D0\u00B5\u00D0\u00B9\u00D0\u00BD \u00D1\u0083\u00D0\u00B7\u00D0\u00BB\u00D0\u00B0 \u00D0\u00B2 \u00D0\u00BD\u00D0\u00B0\u00D1\u0081\u00D1\u0082\u00D0\u00BE\u00D1\u008F\u00D1\u0089\u00D0\u00B5\u00D0\u00B5 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D1\u008F \u00D0\u00B7\u00D0\u00B0\u00D0\u00BD\u00D1\u008F\u00D1\u0082
NO_FLAG_PERMISSION = \u00D0\u00BD\u00D0\u00B5\u00D1\u0082 \u00D1\u0080\u00D0\u00B0\u00D0\u00B7\u00D1\u0080\u00D0\u00B5\u00D1\u0088\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8\u00D1\u008F \u00D0\u00BD\u00D0\u00B0 \u00D1\u0084\u00D0\u00BB\u00D0\u00B0\u00D0\u00B3
OK = OK
ORDER_ALREADY_CLOSED = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7 \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082
ORDER_DOES_NOT_EXIST = \u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D0\u00B0\u00D0\u00B7\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
POLL_ALREADY_EXISTS = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081 \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
POLL_DOES_NOT_EXIST = \u00D0\u00BE\u00D0\u00BF\u00D1\u0080\u00D0\u00BE\u00D1\u0081\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
POLL_OPTION_DOES_NOT_EXIST = \u00D0\u00B2\u00D0\u00B0\u00D1\u0080\u00D0\u00B8\u00D0\u00B0\u00D0\u00BD\u00D1\u0082\u00D0\u00BE\u00D0\u00B2 \u00D0\u00BE\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0082\u00D0\u00B0 \u00D0\u00BD\u00D0\u00B5 \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
PUBLIC_KEY_UNKNOWN = \u00D0\u00BE\u00D1\u0082\u00D0\u00BA\u00D1\u0080\u00D1\u008B\u00D1\u0082\u00D1\u008B\u00D0\u00B9 \u00D0\u00BA\u00D0\u00BB\u00D1\u008E\u00D1\u0087 \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B5\u00D0\u00BD
SELF_SHARE_EXISTS = \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D0\u00B5\u00D0\u00BB\u00D0\u00B8\u00D1\u0082\u00D1\u008C\u00D1\u0081\u00D1\u008F \u00D0\u00B4\u00D0\u00BE\u00D0\u00BB\u00D0\u00B5\u00D0\u00B9
TIMESTAMP_TOO_NEW = \u00D0\u00BD\u00D0\u00BE\u00D0\u00B2\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
TIMESTAMP_TOO_OLD = \u00D1\u0081\u00D1\u0082\u00D0\u00B0\u00D1\u0080\u00D0\u00B0\u00D1\u008F \u00D0\u00BC\u00D0\u00B5\u00D1\u0082\u00D0\u00BA\u00D0\u00B0 \u00D0\u00B2\u00D1\u0080\u00D0\u00B5\u00D0\u00BC\u00D0\u00B5\u00D0\u00BD\u00D0\u00B8
TRANSACTION_ALREADY_CONFIRMED = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0083\u00D0\u00B6\u00D0\u00B5 \u00D0\u00BF\u00D0\u00BE\u00D0\u00B4\u00D1\u0082\u00D0\u00B2\u00D0\u00B5\u00D1\u0080\u00D0\u00B6\u00D0\u00B4\u00D0\u00B5\u00D0\u00BD\u00D0\u00B0
TRANSACTION_ALREADY_EXISTS = \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F \u00D1\u0081\u00D1\u0083\u00D1\u0089\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00B2\u00D1\u0083\u00D0\u00B5\u00D1\u0082
TRANSACTION_UNKNOWN = \u00D0\u00BD\u00D0\u00B5\u00D0\u00B8\u00D0\u00B7\u00D0\u00B2\u00D0\u00B5\u00D1\u0081\u00D1\u0082\u00D0\u00BD\u00D0\u00B0\u00D1\u008F \u00D1\u0082\u00D1\u0080\u00D0\u00B0\u00D0\u00BD\u00D0\u00B7\u00D0\u00B0\u00D0\u00BA\u00D1\u0086\u00D0\u00B8\u00D1\u008F

View File

@@ -171,4 +171,84 @@ public class AccountBalanceTests extends Common {
Common.useDefaultSettings();
}
/** Test batch set/delete of account balances */
@Test
public void testBatchedBalanceChanges() throws DataException, SQLException {
Random random = new Random();
int ai;
try (final Repository repository = RepositoryManager.getRepository()) {
System.out.println("Creating random accounts...");
// Generate some random accounts
List<Account> accounts = new ArrayList<>();
for (ai = 0; ai < 2000; ++ai) {
byte[] publicKey = new byte[32];
random.nextBytes(publicKey);
PublicKeyAccount account = new PublicKeyAccount(repository, publicKey);
accounts.add(account);
}
List<AccountBalanceData> accountBalances = new ArrayList<>();
System.out.println("Setting random balances...");
// Fill with lots of random balances
for (ai = 0; ai < accounts.size(); ++ai) {
Account account = accounts.get(ai);
int assetId = random.nextInt(2);
// random zero, or non-zero, balance
long balance = random.nextBoolean() ? 0L : random.nextInt(100000);
accountBalances.add(new AccountBalanceData(account.getAddress(), assetId, balance));
}
repository.getAccountRepository().setAssetBalances(accountBalances);
repository.saveChanges();
System.out.println("Setting new random balances...");
// Now flip zero-ness for first half of balances
for (ai = 0; ai < accountBalances.size() / 2; ++ai) {
AccountBalanceData accountBalanceData = accountBalances.get(ai);
accountBalanceData.setBalance(accountBalanceData.getBalance() != 0 ? 0L : random.nextInt(100000));
}
// ...and randomize the rest
for (/*use ai from before*/; ai < accountBalances.size(); ++ai) {
AccountBalanceData accountBalanceData = accountBalances.get(ai);
accountBalanceData.setBalance(random.nextBoolean() ? 0L : random.nextInt(100000));
}
repository.getAccountRepository().setAssetBalances(accountBalances);
repository.saveChanges();
System.out.println("Modifying random balances...");
// Fill with lots of random balance changes
for (ai = 0; ai < accounts.size(); ++ai) {
Account account = accounts.get(ai);
int assetId = random.nextInt(2);
// random zero, or non-zero, balance
long balance = random.nextBoolean() ? 0L : random.nextInt(100000);
accountBalances.add(new AccountBalanceData(account.getAddress(), assetId, balance));
}
repository.getAccountRepository().modifyAssetBalances(accountBalances);
repository.saveChanges();
System.out.println("Deleting all balances...");
// Now simply delete all balances
for (ai = 0; ai < accountBalances.size(); ++ai)
accountBalances.get(ai).setBalance(0L);
repository.getAccountRepository().setAssetBalances(accountBalances);
repository.saveChanges();
}
}
}

View File

@@ -1,6 +1,10 @@
package org.qortal.test;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
@@ -133,4 +137,171 @@ public class BlockTests extends Common {
}
}
@Test
public void testLatestBlockCacheWithLatestBlock() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
byte[] parentSignature = latestBlock.getSignature();
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
assertEquals(true, childBlocks.isEmpty());
}
}
@Test
public void testLatestBlockCacheWithPenultimateBlock() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
BlockData penultimateBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - 1);
byte[] parentSignature = penultimateBlock.getSignature();
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
assertEquals(false, childBlocks.isEmpty());
assertEquals(1, childBlocks.size());
BlockData expectedBlock = latestBlock;
BlockData actualBlock = childBlocks.get(0);
assertArrayEquals(expectedBlock.getSignature(), actualBlock.getSignature());
}
}
@Test
public void testLatestBlockCacheWithMiddleBlock() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
int tipOffset = 5;
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
byte[] parentSignature = parentBlock.getSignature();
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
assertEquals(false, childBlocks.isEmpty());
assertEquals(tipOffset, childBlocks.size());
BlockData expectedFirstBlock = repository.getBlockRepository().fromHeight(parentBlock.getHeight() + 1);
BlockData actualFirstBlock = childBlocks.get(0);
assertArrayEquals(expectedFirstBlock.getSignature(), actualFirstBlock.getSignature());
BlockData expectedLastBlock = latestBlock;
BlockData actualLastBlock = childBlocks.get(childBlocks.size() - 1);
assertArrayEquals(expectedLastBlock.getSignature(), actualLastBlock.getSignature());
}
}
@Test
public void testLatestBlockCacheWithFirstBlock() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
int tipOffset = latestBlockCache.size();
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
byte[] parentSignature = parentBlock.getSignature();
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
assertEquals(false, childBlocks.isEmpty());
assertEquals(tipOffset, childBlocks.size());
BlockData expectedFirstBlock = repository.getBlockRepository().fromHeight(parentBlock.getHeight() + 1);
BlockData actualFirstBlock = childBlocks.get(0);
assertArrayEquals(expectedFirstBlock.getSignature(), actualFirstBlock.getSignature());
BlockData expectedLastBlock = latestBlock;
BlockData actualLastBlock = childBlocks.get(childBlocks.size() - 1);
assertArrayEquals(expectedLastBlock.getSignature(), actualLastBlock.getSignature());
}
}
@Test
public void testLatestBlockCacheWithNoncachedBlock() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Deque<BlockData> latestBlockCache = buildLatestBlockCache(repository, 20);
int tipOffset = latestBlockCache.size() + 1; // outside of cache
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset);
byte[] parentSignature = parentBlock.getSignature();
List<BlockData> childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature);
assertEquals(true, childBlocks.isEmpty());
}
}
private Deque<BlockData> buildLatestBlockCache(Repository repository, int count) throws DataException {
Deque<BlockData> latestBlockCache = new LinkedList<>();
// Mint some blocks
for (int h = 0; h < count; ++h)
latestBlockCache.addLast(BlockUtils.mintBlock(repository).getBlockData());
// Reduce cache down to latest 10 blocks
while (latestBlockCache.size() > 10)
latestBlockCache.removeFirst();
return latestBlockCache;
}
private List<BlockData> findCachedChildBlocks(Deque<BlockData> latestBlockCache, byte[] parentSignature) {
return latestBlockCache.stream()
.dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature))
.collect(Collectors.toList());
}
@Test
public void testCommonBlockSearch() {
// Given a list of block summaries, trim all trailing summaries after common block
// We'll represent known block summaries as a list of booleans,
// where the boolean value indicates whether peer's block is also in our repository.
// Trivial case, single element array
assertCommonBlock(0, new boolean[] { true });
// Test odd and even array lengths
for (int arrayLength = 5; arrayLength <= 6; ++arrayLength) {
boolean[] testBlocks = new boolean[arrayLength];
// Test increasing amount of common blocks
for (int c = 1; c <= testBlocks.length; ++c) {
testBlocks[c - 1] = true;
assertCommonBlock(c - 1, testBlocks);
}
}
}
private void assertCommonBlock(int expectedIndex, boolean[] testBlocks) {
int commonBlockIndex = findCommonBlockIndex(testBlocks);
assertEquals(expectedIndex, commonBlockIndex);
}
private int findCommonBlockIndex(boolean[] testBlocks) {
int low = 1;
int high = testBlocks.length - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
if (testBlocks[mid])
low = mid + 1;
else
high = mid - 1;
}
return low - 1;
}
}

View File

@@ -103,8 +103,10 @@ public class ChainWeightTests extends Common {
populateBlockSummariesMinterLevels(repository, shorterChain);
populateBlockSummariesMinterLevels(repository, longerChain);
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain);
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain);
final int mutualHeight = commonBlockHeight - 1 + Math.min(shorterChain.size(), longerChain.size());
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain, mutualHeight);
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain, mutualHeight);
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
}

View File

@@ -4,6 +4,8 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -13,7 +15,11 @@ import org.qortal.test.common.Common;
import static org.junit.Assert.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -71,7 +77,7 @@ public class RepositoryTests extends Common {
}
@Test
public void testDeadlock() throws DataException {
public void testDeadlock() {
// Open connection 1
try (final Repository repository1 = RepositoryManager.getRepository()) {
@@ -92,18 +98,50 @@ public class RepositoryTests extends Common {
// Update account in 1
account1.setConfirmedBalance(Asset.QORT, 5678L);
repository1.saveChanges();
} catch (DataException e) {
fail("deadlock bug");
}
}
@Test
public void testUpdateReadDeadlock() {
// Open connection 1
try (final Repository repository1 = RepositoryManager.getRepository()) {
// Mint blocks so we have data (online account signatures) to work with
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository1);
// Perform database 'update', but don't commit at this stage
repository1.getBlockRepository().trimOldOnlineAccountsSignatures(1, 10);
// Open connection 2
try (final Repository repository2 = RepositoryManager.getRepository()) {
// Perform database read on same blocks - this should not deadlock
repository2.getBlockRepository().getTimestampFromHeight(5);
}
// Save updates - this should not deadlock
repository1.saveChanges();
} catch (DataException e) {
fail("deadlock bug");
}
}
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
@Test
public void testBlockHeightSpeed() throws DataException, SQLException {
final int mintBlockCount = 30000;
try (final Repository repository = RepositoryManager.getRepository()) {
// Mint some blocks
System.out.println("Minting test blocks - should take approx. 30 seconds...");
for (int i = 0; i < 30000; ++i)
System.out.println(String.format("Minting %d test blocks - should take approx. 30 seconds...", mintBlockCount));
long beforeBigMint = System.currentTimeMillis();
for (int i = 0; i < mintBlockCount; ++i)
BlockUtils.mintBlock(repository);
System.out.println(String.format("Minting %d blocks actually took %d seconds", mintBlockCount, (System.currentTimeMillis() - beforeBigMint) / 1000L));
final HSQLDBRepository hsqldb = (HSQLDBRepository) repository;
// Too slow:
@@ -158,6 +196,119 @@ public class RepositoryTests extends Common {
}
}
/**
* Test HSQLDB bug-fix for INSERT INTO...ON DUPLICATE KEY UPDATE... bug
* <p>
* @see <A HREF="https://sourceforge.net/p/hsqldb/discussion/73674/thread/d8d35adb5d/">Behaviour of 'ON DUPLICATE KEY UPDATE'</A> SourceForge discussion
*/
@Test
public void testOnDuplicateKeyUpdateBugFix() throws SQLException, DataException {
ResultSet resultSet;
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
hsqldb.prepareStatement("DROP TABLE IF EXISTS bugtest").execute();
hsqldb.prepareStatement("CREATE TABLE bugtest (id INT NOT NULL, counter INT NOT NULL, PRIMARY KEY(id))").execute();
// No existing row, so new row's "counter" is set to value from VALUES clause, i.e. 1
hsqldb.prepareStatement("INSERT INTO bugtest (id, counter) VALUES (1, 1) ON DUPLICATE KEY UPDATE counter = counter + 1").execute();
resultSet = hsqldb.checkedExecute("SELECT counter FROM bugtest WHERE id = 1");
assertNotNull(resultSet);
assertEquals(1, resultSet.getInt(1));
// Prior to bug-fix, "counter = counter + 1" would always use the 100 from VALUES, instead of existing row's value, for "counter"
hsqldb.prepareStatement("INSERT INTO bugtest (id, counter) VALUES (1, 100) ON DUPLICATE KEY UPDATE counter = counter + 1").execute();
resultSet = hsqldb.checkedExecute("SELECT counter FROM bugtest WHERE id = 1");
assertNotNull(resultSet);
// Prior to bug-fix, this would be 100 + 1 = 101
assertEquals(2, resultSet.getInt(1));
}
}
/**
* Test HSQLDB bug-fix for "General Error" in non-fully-qualified columns inside LATERAL()
* <p>
* @see <A HREF="https://sourceforge.net/p/hsqldb/bugs/1580/">#1580 General error with LATERAL and transitive join column</A> SourceForge ticket
*/
@Test
public void testOnLateralGeneralError() {
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
hsqldb.prepareStatement("DROP TABLE IF EXISTS tableA").execute();
hsqldb.prepareStatement("DROP TABLE IF EXISTS tableB").execute();
hsqldb.prepareStatement("DROP TABLE IF EXISTS tableC").execute();
hsqldb.prepareStatement("CREATE TABLE tableA (col1 INT)").execute();
hsqldb.prepareStatement("CREATE TABLE tableB (col1 INT)").execute();
hsqldb.prepareStatement("CREATE TABLE tableC (col2 INT, PRIMARY KEY (col2))").execute();
// Prior to bug-fix #1580 this would throw a General Error SQL Exception
hsqldb.prepareStatement("SELECT col3 FROM tableA JOIN tableB USING (col1) CROSS JOIN LATERAL(SELECT col2 FROM tableC WHERE col2 = col1) AS tableC (col3)").execute();
} catch (SQLException | DataException e) {
fail("HSQLDB bug #1580");
}
}
/** Specifically test LATERAL() usage in Asset repository */
@Test
public void testAssetLateral() {
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
List<Long> assetIds = Collections.emptyList();
List<Long> otherAssetIds = Collections.emptyList();
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
hsqldb.getAssetRepository().getRecentTrades(assetIds, otherAssetIds, limit, offset, reverse);
} catch (DataException e) {
fail("HSQLDB bug #1580");
}
}
/** Specifically test LATERAL() usage in AT repository */
@Test
public void testAtLateral() {
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
byte[] codeHash = BTCACCT.CODE_BYTES_HASH;
Boolean isFinished = null;
Integer dataByteOffset = null;
Long expectedValue = null;
Integer minimumFinalHeight = 2;
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
hsqldb.getATRepository().getMatchingFinalATStates(codeHash, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse);
} catch (DataException e) {
fail("HSQLDB bug #1580");
}
}
/** Specifically test LATERAL() usage in Chat repository */
@Test
public void testChatLateral() {
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
String address = Crypto.toAddress(new byte[32]);
hsqldb.getChatRepository().getActiveChats(address);
} catch (DataException e) {
fail("HSQLDB bug #1580");
}
}
/** Test batched DELETE */
@Test
public void testBatchedDelete() {
// Generate test data
List<Object[]> batchedObjects = new ArrayList<>();
for (int i = 0; i < 100; ++i)
batchedObjects.add(new Object[] { String.valueOf(i), 1L });
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
hsqldb.deleteBatch("AccountBalances", "account = ? AND asset_id = ?", batchedObjects);
} catch (DataException | SQLException e) {
fail("Batched delete failed: " + e.getMessage());
}
}
public static void hsqldbSleep(int millis) throws SQLException {
System.out.println(String.format("HSQLDB sleep() thread ID: %s", Thread.currentThread().getId()));

View File

@@ -0,0 +1,76 @@
package org.qortal.test;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.AccountUtils;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.transaction.Transaction.TransactionType;
public class TransactionSearchTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testFindingSpecificTransactionsWithinHeight() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount chloe = Common.getTestAccount(repository, "chloe");
// Block 2
BlockUtils.mintBlock(repository);
// Block 3
AccountUtils.pay(repository, alice, chloe.getAddress(), 1234L);
// Block 4
AccountUtils.pay(repository, chloe, alice.getAddress(), 5678L);
// Block 5
BlockUtils.mintBlock(repository);
List<byte[]> signatures;
// No transactions with this type
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(TransactionType.GROUP_KICK, null, null, null);
assertEquals(0, signatures.size());
// 2 payments
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(TransactionType.PAYMENT, null, null, null);
assertEquals(2, signatures.size());
// 1 payment by Alice
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(TransactionType.PAYMENT, alice.getPublicKey(), null, null);
assertEquals(1, signatures.size());
// 1 transaction by Chloe
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, chloe.getPublicKey(), null, null);
assertEquals(1, signatures.size());
// 1 transaction from blocks 4 onwards
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, 4, null);
assertEquals(1, signatures.size());
// 1 transaction from blocks 2 to 3
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, 2, 3);
assertEquals(1, signatures.size());
// No transaction of this type from blocks 2 to 5
signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(TransactionType.ISSUE_ASSET, null, 2, 5);
assertEquals(0, signatures.size());
}
}
}

View File

@@ -7,7 +7,7 @@ import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockMinter;
import org.qortal.controller.BlockMinter;
import org.qortal.data.account.AccountData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;

View File

@@ -9,6 +9,7 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.resource.BlocksResource;
import org.qortal.block.GenesisBlock;
import org.qortal.repository.DataException;
@@ -82,6 +83,19 @@ public class BlockApiTests extends ApiCommon {
@Test
public void testGetBlockRange() {
assertNotNull(this.blocksResource.getBlockRange(1, 1));
List<Integer> testValues = Arrays.asList(null, Integer.valueOf(1));
for (Integer startHeight : testValues)
for (Integer endHeight : testValues)
for (Integer count : testValues) {
if (startHeight != null && endHeight != null && count != null) {
assertApiError(ApiError.INVALID_CRITERIA, () -> this.blocksResource.getBlockRange(startHeight, endHeight, count));
continue;
}
assertNotNull(this.blocksResource.getBlockRange(startHeight, endHeight, count));
}
}
@Test

View File

@@ -0,0 +1,426 @@
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.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
public class AtRepositoryTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testGetATStateAtHeightWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
Integer testHeight = 8;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetATStateAtHeightWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int maxHeight = 8;
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNull(atStateData.getStateData());
}
}
@Test
public void testGetLatestATStateWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetLatestATStatePostTrimming() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
int maxHeight = blockchainHeight + 100; // more than latest block height
Integer testHeight = blockchainHeight;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
assertEquals(testHeight, atStateData.getHeight());
// We should always have the latest AT state data available
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetMatchingFinalATStatesWithoutDataValue() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATData atData = repository.getATRepository().fromATAddress(atAddress);
byte[] codeHash = atData.getCodeHash();
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = null;
Long expectedValue = null;
Integer minimumFinalHeight = null;
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
codeHash,
isFinished,
dataByteOffset,
expectedValue,
minimumFinalHeight,
limit, offset, reverse);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetMatchingFinalATStatesWithDataValue() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight;
ATData atData = repository.getATRepository().fromATAddress(atAddress);
byte[] codeHash = atData.getCodeHash();
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = MachineState.HEADER_LENGTH + 0;
Long expectedValue = 0L;
Integer minimumFinalHeight = null;
Integer limit = null;
Integer offset = null;
Boolean reverse = null;
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(
codeHash,
isFinished,
dataByteOffset,
expectedValue,
minimumFinalHeight,
limit, offset, reverse);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testGetBlockATStatesAtHeightWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
doDeploy(repository, deployer, creationBytes, fundingAmount);
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
Integer testHeight = 8;
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
// getBlockATStatesAtHeight never returns actual AT state data anyway
assertNull(atStateData.getStateData());
}
}
@Test
public void testGetBlockATStatesAtHeightWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
doDeploy(repository, deployer, creationBytes, fundingAmount);
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int maxHeight = 8;
Integer testHeight = maxHeight - 2;
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
List<ATStateData> atStates = repository.getATRepository().getBlockATStatesAtHeight(testHeight);
assertEquals(false, atStates.isEmpty());
assertEquals(1, atStates.size());
ATStateData atStateData = atStates.get(0);
assertEquals(testHeight, atStateData.getHeight());
// getBlockATStatesAtHeight never returns actual AT state data anyway
assertNull(atStateData.getStateData());
}
}
@Test
public void testSaveATStateWithData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight - 2;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
repository.getATRepository().save(atStateData);
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
}
}
@Test
public void testSaveATStateWithoutData() throws DataException {
byte[] creationBytes = buildSimpleAT();
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice");
long fundingAmount = 1_00000000L;
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
String atAddress = deployAtTransaction.getATAccount().getAddress();
// Mint a few blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
int blockchainHeight = repository.getBlockRepository().getBlockchainHeight();
Integer testHeight = blockchainHeight - 2;
ATStateData atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNotNull(atStateData.getStateData());
// Clear data
ATStateData newAtStateData = new ATStateData(atStateData.getATAddress(),
atStateData.getHeight(),
/*StateData*/ null,
atStateData.getStateHash(),
atStateData.getFees(),
atStateData.isInitial());
repository.getATRepository().save(newAtStateData);
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
assertEquals(testHeight, atStateData.getHeight());
assertNull(atStateData.getStateData());
}
}
private byte[] buildSimpleAT() {
// Pretend we use 4 values in data segment
int addrCounter = 4;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
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 = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
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;
}
}

View File

@@ -12,6 +12,7 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
@@ -28,7 +29,7 @@ public class BtcTests extends Common {
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException {
public void testGetMedianBlockTime() throws BlockStoreException, BitcoinException {
System.out.println(String.format("Starting BTC instance..."));
BTC btc = BTC.getInstance();
System.out.println(String.format("BTC instance started"));
@@ -50,7 +51,7 @@ public class BtcTests extends Common {
}
@Test
public void testFindP2shSecret() {
public void testFindP2shSecret() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
@@ -104,4 +105,17 @@ public class BtcTests extends Common {
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws BitcoinException {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = btc.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@@ -16,6 +16,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -135,11 +136,7 @@ public class CheckP2SH {
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
}
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
@@ -164,7 +161,9 @@ public class CheckP2SH {
System.exit(2);
}
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
System.err.println("Repository issue: " + e.getMessage());
} catch (BitcoinException e) {
System.err.println("Bitcoin issue: " + e.getMessage());
}
}

View File

@@ -11,8 +11,11 @@ import org.bitcoinj.script.ScriptBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.junit.Test;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crosschain.BitcoinTransaction;
import org.qortal.crosschain.ElectrumX;
import org.qortal.crosschain.ElectrumX.UnspentOutput;
import org.qortal.crosschain.TransactionHash;
import org.qortal.crosschain.UnspentOutput;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
@@ -34,26 +37,36 @@ public class ElectrumXTests {
}
@Test
public void testGetCurrentHeight() {
public void testGetCurrentHeight() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Integer height = electrumX.getCurrentHeight();
int height = electrumX.getCurrentHeight();
assertNotNull(height);
assertTrue(height > 10000);
System.out.println("Current TEST3 height: " + height);
}
@Test
public void testGetRecentBlocks() {
public void testInvalidRequest() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
try {
electrumX.getBlockHeaders(-1, -1);
} catch (BitcoinException e) {
// Should throw due to negative start block height
return;
}
fail("Negative start block height should cause error");
}
@Test
public void testGetRecentBlocks() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Integer height = electrumX.getCurrentHeight();
assertNotNull(height);
int height = electrumX.getCurrentHeight();
assertTrue(height > 10000);
List<byte[]> recentBlockHeaders = electrumX.getBlockHeaders(height - 11, 11);
assertNotNull(recentBlockHeaders);
System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size()));
for (int i = 0; i < recentBlockHeaders.size(); ++i) {
@@ -67,42 +80,39 @@ public class ElectrumXTests {
}
@Test
public void testGetP2PKHBalance() {
public void testGetP2PKHBalance() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
Long balance = electrumX.getBalance(script);
long balance = electrumX.getConfirmedBalance(script);
assertNotNull(balance);
assertTrue(balance > 0L);
System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L)));
}
@Test
public void testGetP2SHBalance() {
public void testGetP2SHBalance() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
Long balance = electrumX.getBalance(script);
long balance = electrumX.getConfirmedBalance(script);
assertNotNull(balance);
assertTrue(balance > 0L);
System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L)));
}
@Test
public void testGetUnspentOutputs() {
public void testGetUnspentOutputs() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script);
List<UnspentOutput> unspentOutputs = electrumX.getUnspentOutputs(script, false);
assertNotNull(unspentOutputs);
assertFalse(unspentOutputs.isEmpty());
for (UnspentOutput unspentOutput : unspentOutputs)
@@ -110,27 +120,68 @@ public class ElectrumXTests {
}
@Test
public void testGetRawTransaction() {
public void testGetRawTransaction() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes();
byte[] rawTransactionBytes = electrumX.getRawTransaction(txHash);
assertNotNull(rawTransactionBytes);
assertFalse(rawTransactionBytes.length == 0);
}
@Test
public void testGetAddressTransactions() {
public void testGetUnknownRawTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
byte[] txHash = HashCode.fromString("f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0").asBytes();
try {
electrumX.getRawTransaction(txHash);
fail("Bitcoin transaction should be unknown and hence throw exception");
} catch (BitcoinException e) {
if (!(e instanceof BitcoinException.NotFoundException))
fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
}
}
@Test
public void testGetTransaction() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
String txHash = "7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af";
BitcoinTransaction transaction = electrumX.getTransaction(txHash);
assertNotNull(transaction);
assertTrue(transaction.txHash.equals(txHash));
}
@Test
public void testGetUnknownTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
String txHash = "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0";
try {
electrumX.getTransaction(txHash);
fail("Bitcoin transaction should be unknown and hence throw exception");
} catch (BitcoinException e) {
if (!(e instanceof BitcoinException.NotFoundException))
fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
}
}
@Test
public void testGetAddressTransactions() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<byte[]> rawTransactions = electrumX.getAddressTransactions(script);
List<TransactionHash> transactionHashes = electrumX.getAddressTransactions(script, false);
assertNotNull(rawTransactions);
assertFalse(rawTransactions.isEmpty());
assertFalse(transactionHashes.isEmpty());
}
}

View File

@@ -7,6 +7,7 @@ import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.TransactionOutput;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BitcoinException;
import org.qortal.settings.Settings;
import com.google.common.hash.HashCode;
@@ -46,9 +47,11 @@ public class GetTransaction {
}
// Grab all outputs from transaction
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId);
if (fundingOutputs == null) {
System.out.println(String.format("Transaction not found"));
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getOutputs(transactionId);
} catch (BitcoinException e) {
System.out.println(String.format("Transaction not found (or error occurred)"));
return;
}

View File

@@ -0,0 +1,53 @@
package org.qortal.test.btcacct;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class P2shTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
}
@After
public void afterTest() {
BTC.resetForTesting();
}
@Test
public void testFindP2shSecret() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testDetermineP2shStatus() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddress, 1L);
System.out.println(String.format("P2SH %s status: %s", p2shAddress, p2shStatus.name()));
}
}

View File

@@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -136,7 +137,14 @@ public class Redeem {
System.out.println("\nProcessing:");
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
long medianBlockTime;
try {
medianBlockTime = BTC.getInstance().getMedianBlockTime();
} catch (BitcoinException e1) {
System.err.println("Unable to determine median block time");
System.exit(2);
return;
}
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis();
@@ -147,18 +155,24 @@ public class Redeem {
}
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
long p2shBalance;
try {
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
return;
}
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs == null) {
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2);
return;
}
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));

View File

@@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -135,7 +136,14 @@ public class Refund {
System.out.println("\nProcessing:");
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
long medianBlockTime;
try {
medianBlockTime = BTC.getInstance().getMedianBlockTime();
} catch (BitcoinException e) {
System.err.println("Unable to determine median block time");
System.exit(2);
return;
}
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis();
@@ -151,18 +159,24 @@ public class Refund {
}
// Check P2SH is funded
Long p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
long p2shBalance;
try {
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
return;
}
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs == null) {
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2);
return;
}
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
@@ -186,7 +200,7 @@ public class Refund {
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundKey.getPubKeyHash());
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();

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