Compare commits

...

418 Commits

Author SHA1 Message Date
CalDescent
2b6ae57a27 Merge branch 'master' into chat-reference
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
#	src/main/resources/blockchain.json
#	src/test/resources/test-chain-v2-block-timestamps.json
#	src/test/resources/test-chain-v2-disable-reference.json
#	src/test/resources/test-chain-v2-founder-rewards.json
#	src/test/resources/test-chain-v2-leftover-reward.json
#	src/test/resources/test-chain-v2-minting.json
#	src/test/resources/test-chain-v2-qora-holder-extremes.json
#	src/test/resources/test-chain-v2-qora-holder-reduction.json
#	src/test/resources/test-chain-v2-qora-holder.json
#	src/test/resources/test-chain-v2-reward-levels.json
#	src/test/resources/test-chain-v2-reward-scaling.json
#	src/test/resources/test-chain-v2-reward-shares.json
#	src/test/resources/test-chain-v2.json
2022-11-27 20:06:55 +00:00
CalDescent
4e829a2d05 Bump version to 3.7.0 2022-11-07 21:12:34 +00:00
CalDescent
a7402adfa5 Merge branch 'null-owned-groups' 2022-11-06 21:20:19 +00:00
CalDescent
9255df46cf Script updates to support add/remove dev group admins 2022-11-06 19:46:12 +00:00
CalDescent
db22445948 Include API key automatically in publish-auto-update-v5.pl 2022-11-06 14:52:14 +00:00
CalDescent
818e037e75 Merge branch 'master' into null-owned-groups 2022-11-06 13:08:54 +00:00
CalDescent
9c68f1038a Bump AT version to 1.4.0 2022-11-05 14:02:04 +00:00
CalDescent
10ae383bb6 Merge pull request #102 from catbref/faster-qort-buy-PoC
Proof of concept: speed up QORT buying
2022-11-01 18:55:21 +00:00
catbref
aead9cfcbf Proof of concept: speed up QORT buying
When users buy QORT ("Alice"-side), most of the API time is spent computing mempow for the MESSAGE sent to Bob's AT.
This is the final stage startResponse() and after Alice's P2SH is already broadcast.

To speed this up, the MESSAGE part is moved into its own thread allowing startResponse() to return sooner, improving the user experience.

Caveats:
If MESSAGE importAsUnconfirmed() somehow fails the the buy won't complete and Alice will have to wait for P2SH refund.
If Alice shuts down her node while MESSAGE mempow is being computed then it's possible the shutdown will be blocked until mempow is complete.

Currently only implemented in LitecoinACCTv3TradeBot as this is only proof-of-concept.
Tested with multiple buys in the same block.
2022-11-01 08:55:57 +00:00
CalDescent
985c195e9e Added GIF_REPOSITORY, with custom validation function and unit tests. 2022-10-30 17:33:21 +00:00
CalDescent
0628847d14 Removed QORTAL_METADATA service tests. 2022-10-30 17:25:11 +00:00
CalDescent
4043ae1928 Added QCHAT_IMAGE service (with 500KB file size limit). 2022-10-30 17:23:46 +00:00
CalDescent
fa80c83864 Remove QORTAL_METADATA service as this uses its own protocol instead. 2022-10-30 17:07:56 +00:00
CalDescent
f739d8f5c6 Added increaseOnlineAccountsDifficultyTimestamp feature trigger to unit tests. 2022-10-28 18:06:34 +01:00
CalDescent
166425bee9 Added feature trigger timestamp (TBC) to increase online accounts mempow difficulty (also TBC). 2022-10-28 17:20:39 +01:00
CalDescent
59a804c560 Include "blocks remaining" in systray when syncing from more than 60 minutes away from a peer's chain tip. 2022-10-28 16:57:52 +01:00
CalDescent
b64c053531 Reuse the work buffer when verifying online accounts from the OnlineAccountsManager import queue.
This is a hopeful fix for extra memory usage since mempow activated, due to adding a lot of load to the garbage collector. It only applies to accounts verified from the import queue; the optimization hasn't been applied to block processing. But verifying online accounts when processing blocks is rare and generally would only last a short amount of time.
2022-10-28 16:54:53 +01:00
CalDescent
30cd56165a Speed up syncing blocks in the range of 1-12 hours ago by caching the valid online accounts. 2022-10-28 16:02:25 +01:00
CalDescent
510328db47 Removed unused timestamp value. 2022-10-28 15:50:43 +01:00
CalDescent
9d74f0eec0 Added haschatreference, with possible values of true, false, or null, to allow optional filtering by the presence or absense of a chat reference. 2022-10-24 19:21:29 +01:00
CalDescent
09014d07e0 Fixed issues retrieving chatReference from the db. 2022-10-23 19:29:31 +01:00
CalDescent
f83d4bac7b Reduced online accounts mempow difficulty to 5 on testnets.
This allows testnets to more easily coexist on the same machines that are running a mainnet instance, and still tests the mempow computation and verification in a non-resource-intensive way.
2022-10-23 17:01:58 +01:00
CalDescent
b3273ff01a Removed all mempow feature trigger conditionals.
We no longer need all the code complexity, now that 24 hours have passed since activation. We don't validate online accounts beyond 12 hours, and the data is trimmed after 24 hours.
2022-10-23 16:47:42 +01:00
CalDescent
1dd039fb2d Merge branch 'master' into chat-reference 2022-10-23 14:14:23 +01:00
CalDescent
1d5497e484 Modifications to support a single node testnet:
- Added "singleNodeTestnet" setting, allowing for fast and consecutive block minting, and no requirement for a minimum number of peers.
- Added "recoveryModeTimeout" setting (previously hardcoded in Synchronizer).
- Updated testnets documentation to include new settings and a quick start guide.
- Added "generic" minting account that can be used in testnets (not functional on mainnet), to simplify the process for new devs.
2022-10-23 14:13:38 +01:00
CalDescent
b37aa749c6 Removed onlineAccountsMemPoWEnabled setting as it's no longer needed. 2022-10-22 19:34:24 +01:00
CalDescent
e45ad37eb5 Fixed bug which could prevent invalid accounts being removed from the queue until the next valid one is added. 2022-10-22 19:30:08 +01:00
CalDescent
72985b1fc6 Reduce log spam, especially around the time of node startup before online accounts have been retrieved.
We expect a "Couldn't build a to-be-minted block" log on every startup due to trying to mint before having any accounts. This one has moved from error to info level because error logs can be quite intrusive when using an IDE.
2022-10-22 19:24:54 +01:00
CalDescent
6f27d3798c Improved online accounts processing, to avoid creating keys in the map before validation. 2022-10-22 19:18:41 +01:00
CalDescent
23a5c5f9b4 Fixed bug in original commit - we need to save the chat reference to the db. 2022-10-22 12:50:28 +01:00
CalDescent
a4759a0ef4 Re-ordered chat transaction transformation, to simplify UI code. New additions are now at the end of the data bytes. 2022-10-22 12:43:40 +01:00
CalDescent
910191b074 Added optional chatReference field to CHAT transactions.
This allows one message to reference another, e.g. for replies, edits, and reactions. We can't use the existing reference field as this is used for encryption and generally points to the user's lastReference at the time of signing.

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

This needs a feature trigger timestamp to activate, and that same timestamp will need to be used in the UI since that is responsible for building the chat transactions.
2022-10-21 15:58:23 +01:00
CalDescent
57125a91cf Bump version to 3.6.4 2022-10-15 18:59:42 +01:00
CalDescent
3c565638c1 onlineAccountsMemoryPoWTimestamp set to Sat Oct 22 2022 16:00:00 UTC 2022-10-15 18:58:13 +01:00
CalDescent
c2d02aead9 Default minPeerVersion set to 3.6.3 2022-10-14 18:44:25 +01:00
CalDescent
0d9aafaf4e Reduced log spam 2022-10-14 17:03:10 +01:00
CalDescent
3844358380 Mark a peer as misbehaved if it fails to respond with a usable block 3 times in a row.
This should help to workaround deserialization and missing response issues.
2022-10-14 16:38:05 +01:00
CalDescent
b4125d2bf1 Fix for NPE in verifyMemoryPoW() 2022-10-14 11:34:46 +01:00
CalDescent
5c223179ed Updated AdvancedInstaller project for v3.6.3 2022-10-13 23:37:21 +01:00
CalDescent
f3cb57417a Merge branch 'master' of github.com:Qortal/qortal 2022-10-13 23:36:27 +01:00
CalDescent
7c7f071eba Bump version to 3.6.3 2022-10-12 08:54:27 +01:00
CalDescent
7c15d88cbc Fix for issue in BLOCK_SUMMARIES_V2 when sending an empty array of summaries.
The BLOCK_SUMMARIES message type would differentiate between an empty response and a missing/invalid response. However, in V2, a response with empty summaries would throw a BufferUnderflowException and be treated by the caller as a null message.

This caused problems when trying to find a common block with peers that have diverged by more than 8 blocks. With V1 the caller would know to search back further (e.g. 16 blocks) but in V2 it was treated as "no response" and so the caller would give up instead of increasing the look-back threshold.

This fix will identify BLOCK_SUMMARIES_V2 messages with no content, and return an empty array of block summaries instead of a null message.

Should be enough to recover any stuck nodes, as long as they haven't diverged more than 240 blocks from the main chain.
2022-10-12 08:52:58 +01:00
CalDescent
d4aaba2293 Bump version to 3.6.2 2022-10-10 19:06:08 +01:00
CalDescent
10d3176e70 Revert "Always use BlockSummariesMessage V1 (instead of V2) when responding to GetBlockSummaries requests."
This reverts commit 2d58118d7c.
2022-10-10 10:28:44 +01:00
CalDescent
36fcd6792a Discard BLOCK_SUMMARIES_V2 messages with an ID (thanks to @catbref for the code)
This is a better fix for the "contaminated chain tip summaries" issue. Need to reduce the logging level to debug before release.
2022-10-10 10:28:36 +01:00
CalDescent
cb1eee8ff5 GenericUnknownMessage.MINIMUM_PEER_VERSION set to 3.6.1.
This should ideally have been set in the 3.6.1 release, but not setting it is unlikely to have caused any problems.
2022-10-09 20:37:39 +01:00
CalDescent
2d58118d7c Always use BlockSummariesMessage V1 (instead of V2) when responding to GetBlockSummaries requests.
This should hopefully fix a potential issue where peer's chain tip data becomes contaminated with other summary data, causing incorrect sync decisions.
2022-10-09 20:11:01 +01:00
CalDescent
e6bb0b81cf Revert "Reduce INITIAL_BLOCK_STEP from 8 to 7."
This reverts commit 0088ba8485.
2022-10-09 19:11:20 +01:00
CalDescent
77d60fc33f Revert "Skip GET_BLOCK_SUMMARIES requests if it can already be fulfilled entirely from the peer's chain tip block summaries cache."
This reverts commit 8cedf618f4.
2022-10-09 14:11:28 +01:00
CalDescent
504f38b42a Merge pull request #97 from Nuc1eoN/patch-1
Mark start/stop scripts as executables
2022-10-08 19:49:10 +01:00
Nuc1eoN
3a18599d85 Mark start/stop scripts as executables
The `start.sh` & `stop.sh` scripts have already been marked as executables in the source folder... But since we have only piped their contents, we need to set correct file permissions again.
2022-10-07 23:35:35 +02:00
CalDescent
0088ba8485 Reduce INITIAL_BLOCK_STEP from 8 to 7.
This allows the first pass to always be served from the peer's cache of 8 summaries. This allows a maximum of 7 to be returned, because the 8th spot is needed for the parent block's signature.
2022-10-07 14:47:46 +01:00
CalDescent
8cedf618f4 Skip GET_BLOCK_SUMMARIES requests if it can already be fulfilled entirely from the peer's chain tip block summaries cache.
Loading from the cache should speed up sync decisions, particularly when choose which peer to sync from. The greater the number of connected peers, the more significant this optimization will be. It should also reduce wasted network requests and data usage.

Adding this check prior to making a network request is a simple way to introduce the new cached summaries from BLOCK_SUMMARIES_V2 without having to rewrite a lot of the complex sync / peer comparison logic. Longer term we may want to rewrite that logic to read from the cache directly, but it doesn't make sense to introduce that level of risk at this point time, especially as the Synchronizer may be rewritten soon to prefer longer chains.

Even so, this is still quite a high risk commit so lots of testing will be needed.
2022-10-07 14:46:09 +01:00
CalDescent
fdd95eac56 Limit to 240 blocks in syncToPeerChain().
Should fix OutOfMemoryException often seen when syncing from 1000+ blocks behind the chain tip.
2022-10-07 11:05:24 +01:00
CalDescent
10b0f0a054 Catch JSON exceptions in PirateChainWalletController.
This could prevent additional wallets from being initialized if connection was lost while syncing an existing one.
2022-10-05 15:29:29 +01:00
CalDescent
1233ba6703 Bump version to 3.6.1 2022-10-04 20:08:30 +01:00
CalDescent
c35c7180d4 Return empty levels in GET /addresses/online/levels 2022-10-03 10:58:47 +01:00
CalDescent
7080b55aac Reintroduced initial sleep period in block archiver. 2022-09-25 19:43:56 +01:00
CalDescent
3890fa8490 Renamed constant for consistency 2022-09-25 18:46:33 +01:00
CalDescent
a9721bab3d Fixed issue causing startup of various components to be delayed by 30 seconds. 2022-09-25 18:39:56 +01:00
CalDescent
1bb8f1b6d2 Fixed bug in last commit.
We need to track items to remove separately from items to add, otherwise invalid accounts remain in the queue.
2022-09-25 12:36:00 +01:00
CalDescent
765416db71 Yet another attempt to optimize the online accounts import queue processing.
The main difference here is that we now remove items from the onlineAccountsImportQueue in a batch, _after_ they have been imported. This prevents duplicates from being added to the queue in the previous time gap between them being removed and imported.
2022-09-25 12:26:00 +01:00
CalDescent
5989473c8a Revert "Allow duplicate variations of each OnlineAccountData in the import queue, but don't allow two entries that match exactly."
This reverts commit 6d9e6e8d4c.
2022-09-25 12:06:14 +01:00
CalDescent
aa9da45c01 Added optional filtering by reference in GET /chat/messages 2022-09-25 11:38:17 +01:00
CalDescent
4681218416 Include total count in debug trade presence logging 2022-09-24 15:49:29 +01:00
CalDescent
5c746f0bd9 Fixed bug which required a node to hold local trade presences before it would request any.
This caused large gaps with no presence data. They are removed when they expire, causing the local count to drop to zero, and the node would only start requesting them again once a peer had pushed one or more entries proactively.
2022-09-24 15:48:45 +01:00
CalDescent
309f27a6b8 Moved error to debug, as we now get a burst of these soon after startup, due to commit 99858f3.
This also shows that commit 99858f3 now prevents a block candidate with a very small number of online accounts being built immediately after startup.
2022-09-24 15:21:01 +01:00
CalDescent
d2ebb215e6 Fixed Synchronizer.getBlockSummaries() which was expecting BLOCK_SUMMARIES, but updated peers send BLOCK_SUMMARIES_V2 2022-09-24 14:36:49 +01:00
CalDescent
7a60f713ea Fixed error in rebase. 2022-09-24 14:35:02 +01:00
CalDescent
e80dd31fb4 BlockSummariesV2Message.MINIMUM_PEER_VERSION set to 3.6.1 2022-09-24 13:53:27 +01:00
catbref
94cdc10151 Initial work on BLOCK_SUMMARIES_V2, part of a bigger arc to improve synchronization.
Touches quite a few files because:

* Deprecate HEIGHT_V2 because it doesn't contain enough info to be fully useful during sync.
Newer peers will re-use BLOCK_SUMMARIES_V2.

* For newer peers, instead of sending / broadcasting HEIGHT_V2,
send top N block summaries instead, to avoid requests for minor reorgs.

* When responding to GET_BLOCK, and we don't actually have the requested block,
we currently send an empty BLOCK_SUMMARIES message instead of not responding,
which would cause a slow timeout in Synchronizer.

This pattern has spread to other network message response code,
so now we introduce a generic 'unknown' message type for all these cases.

* Remove PeerChainTipData class entirely and re-use BlockSummaryData instead.

* Each Peer instance used to hold PeerChainTipData - essentially single latest block summary - but now holds a List of latest block summaries.

* PeerChainTipData getter/setter methods modified for compatibility at this point in time.

* Repository methods that return BlockSummaryData (or lists of) now try to fully populate them,
including newly added block reference field.

* Re-worked Peer.canUseCommonBlockData() to be more readable

* Cherry-picked patch to Message.fromByteBuffer() to pass an empty, read-only ByteBuffer to subclass fromByteBuffer() methods, instead of null.
This allows natural use of BufferUnderflowException if a subclass tries to use read(), or hasRemaining(), etc. from an empty data-payload message.
Previously this could have caused an NPE.
2022-09-24 13:48:01 +01:00
CalDescent
863a5eff97 Moved various online accounts logs to TRACE level, to make it easier to monitor the queue processing when in DEBUG. 2022-09-24 13:11:28 +01:00
CalDescent
5b81b30974 Modified online accounts request interval, and introduced bursting.
It will now request online accounts every 1 minute instead of every 5 seconds, except for the first 5 minutes following a new online accounts timestamp, in which it will request every 5 seconds (referred to as the "burst" interval). It will also use the burst interval for the first 5 minutes after the node starts.

This is based on the idea that most online accounts arrive soon after a new timestamp begins, and so there is no need to request accounts so frequently after that. This should reduce data usage by a significant amount.

Once mempow is fully rolled out, the "burst" feature can be reduced or removed, since online accounts will be sent ahead of time, generally 15-30 mins prior to the new online accounts timestamp becoming active.
2022-09-24 13:02:27 +01:00
CalDescent
174a779e4c Add accounts from the import queue individually, and then skip future duplicates before unnecessarily validating them again.
This closes a gap where accounts would be moved from onlineAccountsImportQueue to onlineAccountsToAdd, but not yet imported. During this time, there was nothing to stop them from being added to the import queue again, causing duplicate validations.
2022-09-24 10:56:52 +01:00
CalDescent
c7cf33ef78 Set hasOurOnlineAccounts to true if one of our accounts is found before signing. 2022-09-24 10:23:55 +01:00
CalDescent
ea4f4d949b When validating online accounts, enforce mempow if the online account's timestamp is after the feature trigger. 2022-09-23 19:45:59 +01:00
CalDescent
6d9e6e8d4c Allow duplicate variations of each OnlineAccountData in the import queue, but don't allow two entries that match exactly. 2022-09-23 18:46:01 +01:00
CalDescent
99858f3781 Wait 30 seconds after the node starts before computing our online accounts.
This allows some time for initial online account lists to be retrieved, and reduces the chances of the same nonce being computed twice.
2022-09-23 18:28:41 +01:00
CalDescent
84a16157d1 Don't add online accounts to the import queue if they are already validated 2022-09-23 18:02:46 +01:00
CalDescent
49d83650f4 Removed online accounts V2 and V1 messaging, as the V3 format will soon be required due to the nonce values. 2022-09-23 15:25:44 +01:00
CalDescent
951c85faf1 Fixed bug causing error 500 in some cases. 2022-09-20 22:26:30 +01:00
CalDescent
84d42b93e1 Reordered code in Block.mint() to fix potential issue after mempow activates. 2022-09-20 08:50:37 +01:00
CalDescent
b99b1f5d57 Bump version to 3.6.0 2022-09-19 17:29:26 +01:00
CalDescent
952c51ab25 QORA / block reward adjustments set to activate at height 1010000 2022-09-19 17:27:07 +01:00
CalDescent
64ef8ab863 OnlineAccountsV3Message.MIN_PEER_VERSION set to 3.6.0 2022-09-19 16:36:39 +01:00
CalDescent
93fd80e289 Require that add/remove admin transactions can only be created by group members.
For regular groups, we require that the owner adds/removes the admins, so group membership is adequately checked. However for null-owned groups this check is skipped. So we need an additional condition to prevent non-group members from issuing a transaction for approval by the group admins.
2022-09-19 16:34:31 +01:00
CalDescent
5581b83c57 Added initial admin approval features for groups owned by the null account.
* The dev group (ID 1) is owned by the null account with public key 11111111111111111111111111111111
 * To regain access to otherwise blocked owner-based rules, it has different validation logic
 * which applies to groups with this same null owner.
 *
 * The main difference is that approval is required for certain transaction types relating to
 * null-owned groups. This allows existing admins to approve updates to the group (using group's
 * approval threshold) instead of these actions being performed by the owner.
 *
 * Since these apply to all null-owned groups, this allows anyone to update their group to
 * the null owner if they want to take advantage of this decentralized approval system.
 *
 * Currently, the affected transaction types are:
 * - AddGroupAdminTransaction
 * - RemoveGroupAdminTransaction
 *
 * This same approach could ultimately be applied to other group transactions too.
2022-09-19 11:03:06 +01:00
CalDescent
5017072f6c Use path parameter instead of query string. 2022-09-17 13:50:04 +01:00
CalDescent
02ac6dd8c1 Added GET /chat/message/{signature} endpoint.
This will ease the transition to a Q-Chat protocol, where chat messages will no longer be regular transactions.
2022-09-17 13:28:32 +01:00
CalDescent
858269f6cb ChatTransaction MAX_DATA_SIZE increased from 256 to 1024 bytes, to allow for new UI features. 2022-09-17 12:21:56 +01:00
CalDescent
791a9b78ec Added support for Pirate Chain wallets on FreeBSD. 2022-09-17 10:36:25 +01:00
CalDescent
aff49e6bdf Added support for ARRR refunds via /crosschain/htlc/refund/{ataddress} and /crosschain/htlc/refundAll
This could probably be refactored into multiple classes to make the code cleaner, but it is functional for now.
2022-09-17 10:30:10 +01:00
CalDescent
2d29fdca00 Allow BTC trades in redeemAll / refundAll, since most will now be using ACCTv3. 2022-09-16 11:19:10 +01:00
CalDescent
063ef8507b Fix for NPE in last commit 2022-09-11 20:04:51 +01:00
CalDescent
f042b5ca5f If mempow is active, remove any legacy accounts from a to-be-minted block that are missing a nonce. 2022-09-11 11:41:11 +01:00
CalDescent
a10e669554 Allow nonce to be computed for "next" timestamp if mempow is enabled in settings. 2022-09-11 11:36:19 +01:00
CalDescent
501f66ab00 BlockTransformer updates necessary for mempow online accounts.
Using the feature trigger timestamp here should be much less error prone than a whole new block message version. Once mempow has been live for at least 24 hours, the feature trigger can be removed and the code cleaned up, as all online accounts signatures will use the new format from that time onwards (legacy signatures are trimmed after 24 hours).
2022-09-10 18:31:01 +01:00
CalDescent
6003ed3ff7 Revert "Use block's online accounts timestamp (instead of main timestamp) for the mempow hard fork."
This reverts commit 8cca6db316.
2022-09-10 17:23:39 +01:00
CalDescent
03e3619817 Revert "Use onlineAccountTimestamp for all mempow hard fork related code in OnlineAccountsManager too."
This reverts commit 23423102e7.
2022-09-10 17:23:34 +01:00
CalDescent
0e42e7b05a Merge branch 'master' into online-accounts-mempow-v2-block-updates
# Conflicts:
#	src/test/resources/test-chain-v2-no-sig-agg.json
2022-09-10 13:50:26 +01:00
CalDescent
d4fbc1687b Optionally exclude initial data from all trade websockets, using query string parameter excludeInitialData=true
Due to the large amount of data, it can take some time for the request to be processed and data to be transferred. It may make more sense to load the initial state from the standard API, and just use the websockets for updates.
2022-09-09 18:45:51 +01:00
CalDescent
8ffdc9b369 POST /crosschain/htlc/importarchivedtrades moved to POST /admin/repository/importarchivedtrades, as this is a repository operation not an HTLC one. 2022-09-09 18:01:20 +01:00
CalDescent
c883dd44c8 Remove LitecoinACCTv2 as it is no longer being used, and is unsafe. 2022-09-09 17:40:10 +01:00
CalDescent
667530e202 Remove DogecoinACCTv2 as it is no longer being used, and is unsafe. 2022-09-09 17:12:44 +01:00
CalDescent
5807d6e0dc Merge branch 'shares-by-level-rework' 2022-09-09 11:35:56 +01:00
CalDescent
ba4eeed358 Modified GET /arbitrary/resources endpoint (and underlying db queries) to allow filtering names by a list, e.g. "followedNames" or "blockedNames". 2022-09-07 18:42:24 +01:00
CalDescent
82edc4d9f3 OnlineAccountsV3Message MIN_PEER_VERSION set to 3.5.1 2022-09-07 18:26:30 +01:00
CalDescent
2a0d5746e6 Only compute "next" online account signature if mempow hard fork is active.
This minimizes the amount of differences in the first phase of the mempow rollout.
2022-09-04 13:19:32 +01:00
CalDescent
23423102e7 Use onlineAccountTimestamp for all mempow hard fork related code in OnlineAccountsManager too. 2022-09-04 11:51:24 +01:00
CalDescent
8879ec5bb4 Avoid computing the proof of work for each online account more than once. 2022-09-03 17:11:13 +01:00
CalDescent
8cca6db316 Use block's online accounts timestamp (instead of main timestamp) for the mempow hard fork.
This ensures that we cleanly switch from the old to new online account format, even if a block is minted retroactively.
2022-09-03 16:30:03 +01:00
CalDescent
effe1ac44d Merge branch 'master' into online-accounts-mempow-v2-block-updates
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
#	src/main/resources/blockchain.json
#	src/test/resources/test-chain-v2-no-sig-agg.json
2022-09-03 15:16:01 +01:00
CalDescent
ad4308afdf Added POST /crosschain/htlc/importarchivedtrades API endpoint.
This imports all trades from TradeBotStatesArchive.json into the repository.
2022-09-02 19:12:36 +01:00
CalDescent
6cfd85bdce Skip over ARRR orders in /refundAll and /redeemAll, as ARRR support hasn't been added for these yet.
This should fix 500 error which could prevent forced refund attempts for LTC and other chains. It wouldn't have affected any normal trade-bot refund functionality.
2022-09-02 18:05:12 +01:00
CalDescent
8b61247712 Added shareBinsByLevelV2.
This allows for different share bin distribution starting at an undecided future block height. This height will correspond with the QORA reduction. New values decided in recent community vote.
2022-09-02 16:43:46 +01:00
CalDescent
a9267760eb qoraHoldersShare reworked to qoraHoldersShareByHeight.
This allows the QORA share percentage to be modified at different heights, based on community votes. Added unit test to simulate a reduction.

# Conflicts:
#	src/test/java/org/qortal/test/minting/RewardTests.java
2022-08-29 09:40:20 +01:00
CalDescent
73396490ba Set walletsPath and listsPath to AppData folder for new Windows installs. 2022-08-27 19:44:31 +01:00
CalDescent
0b8fcc0a7b Bump version to 3.5.0 2022-08-26 19:32:58 +01:00
CalDescent
3d3ecbfb15 Merge branch 'master' into pirate-chain 2022-08-23 09:51:15 +01:00
CalDescent
9658f0cdd4 Revert "Rewrite of isNotOldPeer predicate, to fix logic issue."
This rewrite may have been causing problems with connections in the network, due to peers being forgotten too easily. Reverting for now to see if it solves the problem.

This reverts commit d81071f254.

# Conflicts:
#	src/main/java/org/qortal/network/Network.java
2022-08-21 12:18:40 +01:00
CalDescent
b23500fdd0 Attempt to hold peer connections for 1-4 hours instead of 5-60 mins, as the constant disconnections are causing too much data to be sent over and over.
If this proves to not have any significant bad effects on re-orgs, we could consider setting these even higher or even disabling the auto disconnect by default.
2022-08-21 12:16:44 +01:00
CalDescent
a1365e57d8 Don't log network stats if no messages have been received, as otherwise this floods the logs with empty stats due to failed connections. 2022-08-21 12:14:12 +01:00
CalDescent
d8ca3a455d Merge pull request #93 from catbref/peer-message-stats
Log count & total size of peer messages sent & received when a peer is disconnected
2022-08-21 11:41:37 +01:00
CalDescent
dcc943a906 Hopeful fix for InvalidKeyException seen in some JDK implementations. 2022-08-20 15:33:52 +01:00
CalDescent
cd2010bd06 More algo logging. 2022-08-20 12:50:24 +01:00
CalDescent
8cd16792a2 More logging relating to decryption failures. 2022-08-20 12:45:20 +01:00
CalDescent
4d97586f82 Log exception if AES decryption fails using specific algorithm settings. 2022-08-20 12:27:17 +01:00
CalDescent
3612fd8257 Removed unused throw 2022-08-20 11:58:16 +01:00
CalDescent
ff96868bd9 Log full stack trace if loading a QDN resource fails due to a DataException. 2022-08-20 11:57:53 +01:00
CalDescent
1694d4552e ArbitraryDataReader.deleteWorkingDirectory() is now optional. 2022-08-20 11:57:04 +01:00
CalDescent
bb1593efd2 Log full IOException stacktrace when obtaining Pirate Chain libraries. 2022-08-20 09:47:37 +01:00
CalDescent
4140546afb Ignore failures when deleting original QDN compressed file. 2022-08-20 09:45:48 +01:00
CalDescent
19197812d3 Log DataException during transaction validation. 2022-08-19 22:54:52 +01:00
CalDescent
168d32a474 Include memo for outgoing ARRR transactions. 2022-08-17 19:41:43 +01:00
CalDescent
a4fade0157 Validate wallet initialization result when restoring existing wallet. 2022-08-17 19:27:13 +01:00
CalDescent
2ea6921b66 Added more ARRR lightwalletd nodes 2022-08-17 19:26:47 +01:00
CalDescent
11ef31215b Fixed failing test 2022-08-17 19:25:45 +01:00
CalDescent
830a608b14 Include memo for incoming ARRR transactions. 2022-08-17 19:23:54 +01:00
CalDescent
57acf7dffe Updated text when downloading wallet files from QDN 2022-08-15 20:09:16 +01:00
CalDescent
9debebe03e Default birthday for ARRR moved to "arrrDefaultBirthday" (default 2000000).
This allows users to increase their default birthday if they know that no wallets were created before a certain block, to reduce sync time. It also fixed some failed unit tests that relied on transactions between blocks 1900000 and 2000000.
2022-08-13 16:48:03 +01:00
catbref
b17e96e121 Log count & total size of peer messages sent & received when a peer is disconnected. Requires org.qortal.net.Peer logging level set to DEBUG 2022-08-13 15:42:24 +01:00
CalDescent
b46c3cf95f Allow direct connection QDN retries every hour, instead of every 24 hours. 2022-08-13 14:20:10 +01:00
CalDescent
86526507a6 Increase time range and total number of attempts to fetch a QDN resource, as it previously gave up too quickly. 2022-08-12 22:25:35 +01:00
CalDescent
1b9128289f Added initial support to download Pirate wallet libraries from QDN, using hardcoded transaction signature.
Using a hardcoded signature ensures that the libraries cannot be swapped out without a core auto update, which requires the standard dev team approval process.
2022-08-12 21:57:31 +01:00
CalDescent
4a58f90223 Added support for multiple lightwalletd servers. 2022-08-12 21:52:02 +01:00
CalDescent
e68db40d91 Remove sapling params 2022-08-12 21:50:41 +01:00
CalDescent
bd6c0c9a7d qdata utility renamed to qdn 2022-08-12 13:38:30 +01:00
CalDescent
5804b9469c Default qdata port set to 12391 2022-08-12 13:38:03 +01:00
CalDescent
53b47023ac ARRR default birthday increased to 2000000 2022-08-07 19:15:19 +01:00
CalDescent
22f9f08885 Added Windows amd64 architecture (again with a temporary path) 2022-08-07 19:14:21 +01:00
CalDescent
f26267e572 Don't sync, save, or load null seed wallets. They only act as a placeholder wallet when redeeming/refunding a P2SH. 2022-08-07 18:38:27 +01:00
CalDescent
e8c29226a1 Merge branch 'master' into pirate-chain 2022-08-06 16:41:18 +01:00
CalDescent
94f48f8f54 Don't allow QORT to be listed on the ARRR market unless the Pirate light wallet library is loaded. 2022-08-06 11:48:51 +01:00
CalDescent
3aac580f2c Add amd64 architecture (using a temporary path) 2022-08-06 11:47:36 +01:00
CalDescent
2d0b035f98 Updated AdvancedInstaller project for v3.4.3 2022-08-01 19:58:19 +01:00
CalDescent
075385d3ff Bump version to 3.4.3 2022-07-31 19:50:31 +01:00
CalDescent
6ed8250301 onlineAccountsModulusV2Timestamp set to Sat, 06 Aug 2022 16:00:00 UTC 2022-07-31 19:50:05 +01:00
CalDescent
d10ff49dcb Replaced arm architecture with aarch64, as 32 bit is unsupported. 2022-07-31 19:43:49 +01:00
CalDescent
4cf34fa932 Merge branch 'master' into pirate-chain 2022-07-31 16:00:52 +01:00
CalDescent
06b5d5f1d0 Merge branch 'increase-online-timestamp-modulus' 2022-07-31 13:00:47 +01:00
CalDescent
d6d2641cad Added "bitcoinjLookaheadSize" setting (default 50).
This replaces the WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ constant.
2022-07-31 12:07:44 +01:00
CalDescent
e71f22fd2c Added "gapLimit" setting.
This replaces the previously hardcoded "numberOfAdditionalBatchesToSearch" variable, and specifies the minimum number of empty consecutive addresses required before a set of wallet transactions is considered complete. Used for foreign transaction lists and balances.
2022-07-31 11:59:14 +01:00
CalDescent
c996633732 Added trace level logging. 2022-07-30 19:06:04 +01:00
CalDescent
55f973af3c Ensure all online accounts timestamps are a multiple of the online timestamp modulus.
This is a simple way to discard the 5-minute online account timestamps (from out of date nodes) once the switch to 30-minute online account timestamps has taken place.
2022-07-30 18:15:16 +01:00
CalDescent
fe9744eec6 Fixed missing feature trigger in testchain config 2022-07-30 14:52:47 +01:00
CalDescent
410fa59430 Merge branch 'master' into increase-online-timestamp-modulus
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
2022-07-30 12:23:25 +01:00
CalDescent
522ae2bce7 Updated AdvancedInstaller project for v3.4.2 2022-07-27 22:52:48 +01:00
CalDescent
a6e79947b8 Bump version to 3.4.2 2022-07-23 18:07:35 +01:00
CalDescent
b9bf945fd8 Removed aggregateSignatureTimestamp. All online account signatures are aggregated - there is no need for backwards support as signatures are trimmed from blocks after 24 hours. testOnlineAccountsModulusV2() had to be removed as this relied on pre-aggregation signatures. 2022-07-22 13:57:40 +01:00
CalDescent
85a27c14b8 Revert incorrect genesis timestamp that somehow made it into the stashed code. 2022-07-20 10:38:58 +01:00
CalDescent
46c40ca9ca Committed stashed code that is functional but probably too messy for production use.
Online account nonces are appended to the onlineAccountsSignatures to avoid the need for a new field, but it probably makes more sense to separate them.
2022-07-20 10:27:09 +01:00
CalDescent
fcd0d71cb6 Merge branch 'share-bin-activation' 2022-07-17 19:20:19 +01:00
catbref
275bee62d9 Revert BlockMinter to using long-lifetime repository session.
Although BlockMinter could reattach a repository session to its cache of potential blocks,
and these blocks would in turn reattach that repository session to their transactions,
further transaction-specific fields (e.g. creator PublicKeyAccount) were not being updated.

This would lead to NPEs like the following:

Exception in thread "BlockMinter" java.lang.NullPointerException
        at org.qortal.repository.hsqldb.HSQLDBRepository.cachePreparedStatement(HSQLDBRepository.java:587)
        at org.qortal.repository.hsqldb.HSQLDBRepository.prepareStatement(HSQLDBRepository.java:569)
        at org.qortal.repository.hsqldb.HSQLDBRepository.checkedExecute(HSQLDBRepository.java:609)
        at org.qortal.repository.hsqldb.HSQLDBAccountRepository.getBalance(HSQLDBAccountRepository.java:327)
        at org.qortal.account.Account.getConfirmedBalance(Account.java:72)
        at org.qortal.transaction.MessageTransaction.isValid(MessageTransaction.java:200)
        at org.qortal.block.Block.areTransactionsValid(Block.java:1190)
        at org.qortal.block.Block.isValid(Block.java:1137)
        at org.qortal.controller.BlockMinter.run(BlockMinter.java:301)

where the Account has an associated repository session which is now obsolete.

This commit reverts BlockMinter back to obtaining a repository session before entering main loop.
2022-07-17 13:53:07 +01:00
CalDescent
97221a4449 Added test to simulate level 7-8 reward tier activation, including orphaning. 2022-07-17 13:37:52 +01:00
CalDescent
508a34684b Revert "qoraHoldersShare reworked to qoraHoldersShareByHeight."
This reverts commit 90e8cfc737.

# Conflicts:
#	src/test/java/org/qortal/test/minting/RewardTests.java
2022-07-16 18:45:49 +01:00
CalDescent
3d2144f303 Check orphaning in levels 7-8 and 9-10 reward tests. This would have been tested in orphanCheck() anyway, but this makes the testing a bit more granular. 2022-07-16 17:29:16 +01:00
CalDescent
3c7fbed709 Fixed build error due to merge. 2022-07-15 11:53:30 +01:00
CalDescent
fb9a155e4c Merge branch 'master' into pirate-chain
# Conflicts:
#	src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java
#	src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
2022-07-15 11:46:45 +01:00
CalDescent
fbcc870d36 Added informational test to compare ConsiceSet size against an int array for online account nonce arrays. 2022-07-15 10:23:14 +01:00
CalDescent
020e59743b Fixed failing test(s) due to merge. 2022-07-10 19:49:24 +01:00
CalDescent
0904de3f71 Merge branch 'master' into online-accounts-mempow-v2
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
2022-07-10 16:50:28 +01:00
CalDescent
35f3430687 Added share bin activation feature.
To prevent a single or very small number of minters receiving the rewards for an entire tier, share bins can now require "activation". This adds the requirement that a minimum number of accounts must be present in a share bin before it is considered active. When inactive, the rewards and minters are added to the previous tier.

Summary of new functionality:

- If a share bin has more than one, but less than 30 accounts present, the rewards and accounts are shifted to the previous share bin.
- This process is iterative, so the accounts can shift through multiple tiers until the minimum number of accounts is met, OR the share bin's starting level is less than shareBinActivationMinLevel.
- Applies to level 7+, so that no backwards support is needed. It will only take effect once the first account reaches level 7.

This requires hot swapping the sharesByLevel data to combine tiers where needed, so is a considerable shift away from the immutable array that was in place previously.

All existing and new unit tests are now passing, however a lot more testing will be needed.
2022-07-10 12:09:44 +01:00
CalDescent
90e8cfc737 qoraHoldersShare reworked to qoraHoldersShareByHeight.
This allows the QORA share percentage to be modified at different heights, based on community votes. Added unit test to simulate a reduction.
2022-07-08 11:12:58 +01:00
CalDescent
57bd3c3459 Merge remote-tracking branch 'catbref/auto-update-fix' 2022-07-07 18:48:39 +01:00
CalDescent
ad0d8fac91 Bump version to 3.4.1 2022-07-05 20:56:40 +01:00
CalDescent
a8b58d2007 Reward share limit activation timestamp set to 1657382400000 (Sat Jul 09 2022 16:00:00 UTC) 2022-07-05 20:34:23 +01:00
CalDescent
a099ecf55b Merge branch 'reduce-reward-shares' 2022-07-04 19:58:48 +01:00
CalDescent
6b91b0477d Added version query string param to /blocks/signature/{signature}/data API endpoint, to allow for optional V2 block serialization (with a single combined AT states hash).
Version can only be specified when querying unarchived blocks; archived blocks require V1 for now (and possibly V2 in the future).
2022-07-04 19:57:54 +01:00
CalDescent
fe2c63e8e4 Generate random nonces for test accounts.
These don't have to be valid for unit tests, because they are treated as "cached already valid accounts" in the block validation.
2022-07-02 17:30:31 +01:00
CalDescent
a3febdf00e Pass timestamp to OnlineAccountsManager.isMemoryPoWActive() so that block timestamp can be used. 2022-07-02 17:26:53 +01:00
CalDescent
4ca174fa0b Fixed bug in isMemoryPoWActive() which affected the ability to enable mempow via settings. 2022-07-02 13:45:39 +01:00
CalDescent
294582f136 Added mempow support in OnlineAccountsManager.
- This adds mempow requirements to online account importing (after activation timestamp), however doesn't yet add any requirements to block validation.
- It also causes the 'next' online accounts timestamp to be computed in addition to the 'current', so that the computed nonce value is ready when the next online accounts timestamp window begins.
2022-07-02 12:33:02 +01:00
CalDescent
d7e7c1f48c Fixed bugs from merge conflict, causing incorrect systray statuses in some cases. 2022-07-02 10:33:00 +01:00
CalDescent
215800fb67 Added optional "timeout" parameter to MemoryPoW.compute2().
This can be used to give up after the specified number of milliseconds.
2022-07-01 22:36:51 +01:00
CalDescent
b05d428b2e Added onlineAccountsMemPoWEnabled setting (for beta testing) 2022-07-01 22:31:51 +01:00
CalDescent
d2adadb600 Added onlineAccountsMemoryPoWTimestamp to blockchain.json 2022-07-01 22:31:30 +01:00
CalDescent
8e8c0b3fc5 Added OnlineAccountsV3Message, along with optional nonce Integer in OnlineAccountData.
This could potentially be released ahead of the other mempow code, splitting the rollout into multiple smaller phases.
2022-07-01 22:29:05 +01:00
CalDescent
65d63487f3 Merge branch 'master' into increase-online-timestamp-modulus 2022-07-01 17:46:35 +01:00
CalDescent
7c5932a512 GET /admin/status now returns online account submission status for "isMintingPossible", instead of BlockMinter status.
Online account credit is a more useful definition of "minting" than block signing, from the user's perspective. Should bring UI minting/syncing status in line with the core's systray status.
2022-07-01 17:29:15 +01:00
CalDescent
610a3fcf83 Improved order in getNodeType() 2022-07-01 16:48:57 +01:00
CalDescent
b329dc41bc Updated incorrect ONLINE_ACCOUNTS_V3_PEER_VERSION to 3.4.0 2022-07-01 13:36:56 +01:00
CalDescent
ff78606153 Merge branch 'master' into increase-online-timestamp-modulus 2022-07-01 13:14:15 +01:00
CalDescent
ef249066cd Updated another reference of SimpleTransaction::getTimestamp 2022-07-01 13:13:55 +01:00
CalDescent
80188629df Don't aggregate signatures when running OnlineAccountsTests, as it's too difficult to check how many unique signatures exist over a given period of time. 2022-07-01 13:13:22 +01:00
CalDescent
f77093731c Merge branch 'master' into increase-online-timestamp-modulus
# Conflicts:
#	src/main/java/org/qortal/block/Block.java
#	src/main/java/org/qortal/controller/OnlineAccountsManager.java
2022-07-01 13:03:19 +01:00
CalDescent
ca7d58c272 SimpleTransaction.timestamp is now in milliseconds instead of seconds.
Should fix 1970 timestamp issue in UI for foreign transactions, and also maintains consistency with QORT wallet transactions.
2022-07-01 12:46:20 +01:00
CalDescent
08f3351a7a Reward share transaction modifications:
- Reduce concurrent reward share limit from 6 to 3 (or from 5 to 2 when including self share) - as per community vote.
- Founders remain at 6 (5 when including self share) - also decided in community vote.
- When all slots are being filled, require that at least one is a self share, so that not all can be used for sponsorship.
- Activates at future undecided timestamp.
2022-07-01 12:18:48 +01:00
QuickMythril
f499ada94c Merge pull request #91 from qortish/master
Update SysTray_sv.properties
2022-06-29 08:44:47 -04:00
qortish
f073040c06 Update SysTray_sv.properties
proper
2022-06-29 14:04:27 +02:00
CalDescent
49bfb43bd2 Updated AdvancedInstaller project for v3.4.0 2022-06-28 22:56:11 +01:00
CalDescent
425c70719c Bump version to 3.4.0 2022-06-28 19:29:09 +01:00
CalDescent
1420aea600 aggregateSignatureTimestamp set to 1656864000000 (Sun Jul 03 2022 16:00:00 UTC) 2022-06-28 19:26:00 +01:00
CalDescent
4543062700 Updated blockchain.json files in unit tests to include an already active "aggregateSignatureTimestamp" 2022-06-28 19:22:54 +01:00
CalDescent
722468a859 Restrict relays to v3.4.0 peers and above, in attempt to avoid bugs causing older peers to break relay chains. 2022-06-27 19:38:30 +01:00
CalDescent
492a9ed3cf Fixed more message rebroadcasts that were missing IDs. 2022-06-26 20:02:08 +01:00
CalDescent
420b577606 No longer adding inferior chain signatures in comparePeers() as it doesn't seem 100% reliable in some cases. It's better to re-check weights on each pass. 2022-06-26 18:24:33 +01:00
CalDescent
434038fd12 Reduced online accounts log spam 2022-06-26 16:34:04 +01:00
CalDescent
a9b154b783 Modified BlockMinter.higherWeightChainExists() so that it checks for invalid blocks before treating a chain as higher weight. Otherwise minting is slowed down when a higher weight but invalid chain exists on the network (e.g. after a hard fork). 2022-06-26 15:54:41 +01:00
CalDescent
a01652b816 Removed hasInvalidBlock filtering, as this was unnecessary risk now that the original bug in comparePeers() is fixed. 2022-06-26 10:09:24 +01:00
CalDescent
4440e82bb9 Fixed long term bug in comparePeers() causing peers with invalid blocks to prevent alternate valid but lower weight candidates from being chosen. 2022-06-25 16:34:42 +01:00
CalDescent
a2e1efab90 Synchronize hasInvalidBlock predicate, as it wasn't thread safe 2022-06-25 14:12:21 +01:00
CalDescent
7e1ce38f0a Fixed major bug in hasInvalidBlock predicate 2022-06-25 14:11:25 +01:00
CalDescent
a93bae616e Invalid signatures are now stored as ByteArray instead of String, to avoid regular Base58 encoding and decoding, which is very inefficient. 2022-06-25 13:29:53 +01:00
CalDescent
a2568936a0 Synchronizer: filter out peers reporting to hold invalid block signatures.
We already mark peers as misbehaved if they returned invalid signatures, but this wasn't sufficient when multiple copies of the same invalid block exist on the network (e.g. after a hard fork). In these cases, we need to be more proactive to avoid syncing with these peers, to increase the chances of preserving other candidate blocks.
2022-06-25 12:45:19 +01:00
CalDescent
23408827b3 Merge remote-tracking branch 'catbref/schnorr-agg-BlockMinter-fix' into schnorr-agg-BlockMinter-fix
# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
#	src/main/java/org/qortal/controller/OnlineAccountsManager.java
#	src/main/java/org/qortal/network/message/BlockV2Message.java
#	src/main/resources/blockchain.json
#	src/test/resources/test-chain-v2.json
2022-06-24 11:47:58 +01:00
CalDescent
ae6e2fab6f Rewrite of isNotOldPeer predicate, to fix logic issue (second attempt - first had too many issues)
Previously, a peer would be continuously considered not 'old' if it had a connection attempt in the past day. This prevented some peers from being removed, causing nodes to hold a large repository of peers. On slower systems, this large number of known peers resulted in low numbers of outbound connections being made, presumably because of the time taken to iterate through dataset, using up a lot of allKnownPeers lock time.

On devices that experienced the problem, it could be solved by deleting all known peers. This adds confidence that the old peers were the problem.
2022-06-24 10:36:06 +01:00
CalDescent
3af36644c0 Revert "Rewrite of isNotOldPeer predicate, to fix logic issue."
This reverts commit d81071f254.
2022-06-24 10:26:39 +01:00
CalDescent
db8f627f1a Default minPeerVersion set to 3.3.7 2022-06-24 10:13:55 +01:00
CalDescent
5db0fa080b Prune peers every 5 minutes instead of every cycle of the Controller thread.
This should reduce the amount of time the allKnownPeers lock is held.
2022-06-24 10:13:36 +01:00
CalDescent
d81071f254 Rewrite of isNotOldPeer predicate, to fix logic issue.
Previously, a peer would be continuously considered not 'old' if it had a connection attempt in the past day. This prevented some peers from being removed, causing nodes to hold a large repository of peers. On slower systems, this large number of known peers resulted in low numbers of outbound connections being made, presumably because of the time taken to iterate through dataset, using up a lot of allKnownPeers lock time.

On devices that experienced the problem, it could be solved by deleting all known peers. This adds confidence that the old peers were the problem.
2022-06-24 10:11:46 +01:00
QuickMythril
ba148dfd88 Added Korean translations
credit: TL (Discord username)
2022-06-23 02:35:42 -04:00
CalDescent
dbcb457a04 Merge branch 'master' of github.com:Qortal/qortal 2022-06-20 22:51:33 +01:00
CalDescent
b00e1c8f47 Allow online account submission in all cases when in recovery mode. 2022-06-20 22:50:41 +01:00
CalDescent
899a6eb104 Rework of systray statuses
- Show "Minting" as long as online accounts are submitted to the network (previously it related to block signing).
- Fixed bug causing it to regularly show "Synchronizing 100%".
- Only show "Synchronizing" if the chain falls more than 2 hours behind - anything less is unnecessary noise.
2022-06-20 22:48:32 +01:00
CalDescent
6e556c82a3 Updated AdvancedInstaller project for v3.3.7 2022-06-20 22:25:40 +01:00
CalDescent
35ce64cc3a Bump version to 3.3.7 2022-06-20 21:52:41 +01:00
CalDescent
09b218d16c Merge branch 'master' of github.com:Qortal/qortal 2022-06-20 21:51:39 +01:00
CalDescent
7ea451e027 Allow trades to be initiated, and QDN data to be published, as long as the latest block is within 60 minutes of the current time. Again this should remove negative effects of larger re-orgs from the UX. 2022-06-20 21:26:48 +01:00
CalDescent
ffb27c3946 Further relaxed min latest block timestamp age to be considered "up to date" in a few places, from 30 to 60 mins. This should help reduce the visible effects of larger re-orgs if they happen again. 2022-06-20 21:25:14 +01:00
QuickMythril
6e7d2b50a0 Added Romanian translations
credit: Ovidiu (Telegram username)
2022-06-20 01:27:28 -04:00
QuickMythril
bd025f30ff Updated ApiError German translations
credit: CD (Discord username)
2022-06-20 00:56:19 -04:00
CalDescent
c6cbd8e826 Revert "Sync behaviour changes:"
This reverts commit 8a76c6c0de.
2022-06-19 19:10:37 +01:00
CalDescent
b85afe3ca7 Revert "Keep existing findCommonBlocksWithPeers() and comparePeers() behaviour prior to consensus switchover, to reduce the number of variables."
This reverts commit fecfac5ad9.
2022-06-19 19:10:30 +01:00
CalDescent
5a4674c973 Revert "newConsensusTimestamp set to Sun Jun 19 2022 16:00:00 UTC"
This reverts commit 55a0c10855.
2022-06-19 19:09:57 +01:00
CalDescent
769418e5ae Revert "Fixed unit tests due to missing feature trigger"
This reverts commit 28f9df7178.
2022-06-19 19:09:52 +01:00
CalDescent
38faed5799 Don't submit online accounts if node is more than 2 hours of sync. 2022-06-19 18:56:08 +01:00
catbref
10a578428b Improve intermittent auto-update failures, mostly under non-Windows environments.
Symptoms are:
* AutoUpdate trying to run new ApplyUpdate process, but nothing appears in log-apply-update.?.txt
* Main qortal.jar process continues to run without updating
* Last AutoUpdate line in log.txt.? is:
	2022-06-18 15:42:46 INFO  AutoUpdate:258 - Applying update with: /usr/local/openjdk11/bin/java -Djava.net.preferIPv4Stack=false -Xss256k -Xmx1024m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:5005 -cp new-qortal.jar org.qortal.ApplyUpdate

Changes are:
* child process now inherits parent's stdout / stderr (was piped from parent)
* child process is given a fresh stdin, which is immediately closed
* AutoUpdate now converts -agentlib JVM arg to -DQORTAL_agentlib
* ApplyUpdate converts -DQORTAL_agentlib to -agentlib

The latter two changes are to prevent a conflict where two processes try to reuse the same JVM debugging port number.
2022-06-19 18:25:00 +01:00
CalDescent
96cdf4a87e Updated AdvancedInstaller project for v3.3.6 2022-06-19 11:41:50 +01:00
CalDescent
c0b1580561 Bump version to 3.3.6 2022-06-17 20:42:51 +01:00
CalDescent
28f9df7178 Fixed unit tests due to missing feature trigger 2022-06-17 13:07:33 +01:00
CalDescent
55a0c10855 newConsensusTimestamp set to Sun Jun 19 2022 16:00:00 UTC 2022-06-17 13:04:04 +01:00
CalDescent
7c5165763d Merge branch 'sync-long-tip' 2022-06-17 13:02:08 +01:00
CalDescent
d2836ebcb9 Make sure to set the ID when rebroadcasting messages. Hopeful fix for QDN networking issues. 2022-06-16 22:39:51 +01:00
CalDescent
fecfac5ad9 Keep existing findCommonBlocksWithPeers() and comparePeers() behaviour prior to consensus switchover, to reduce the number of variables. 2022-06-16 18:38:27 +01:00
CalDescent
5ed1ec8809 Merge remote-tracking branch 'catbref/sync-long-tip' into sync-long-tip 2022-06-16 18:35:16 +01:00
catbref
431cbf01af BlockMinter will discard block candidates that turn out to be invalid just prior to adding transactions, to be potentially reminted in the next pass 2022-06-16 17:47:08 +01:00
CalDescent
af792dfc06 Updated AdvancedInstaller project for v3.3.5 2022-06-15 21:11:12 +01:00
CalDescent
d3b6c5f052 Bump version to 3.3.5 2022-06-14 23:18:46 +01:00
CalDescent
f48eb27f00 Revert "Temporarily limit block minter as first stage of multipart minting fix. This will be reverted almost immediately after release."
This reverts commit 0eebfe4a8c.
2022-06-14 22:42:44 +01:00
CalDescent
b02ac2561f Revert "Safety check - also to be removed shortly."
This reverts commit 8b3f9db497.
2022-06-14 22:42:38 +01:00
CalDescent
1b2f66b201 Bump version to 3.3.4 2022-06-13 23:40:39 +01:00
CalDescent
e992f6b683 Increase column size to allow for approx 10x as many online accounts in each block. 2022-06-13 22:51:48 +01:00
CalDescent
8b3f9db497 Safety check - also to be removed shortly. 2022-06-13 22:41:10 +01:00
CalDescent
0eebfe4a8c Temporarily limit block minter as first stage of multipart minting fix. This will be reverted almost immediately after release. 2022-06-13 22:40:54 +01:00
CalDescent
12b3fc257b Updated AdvancedInstaller project for v3.3.3 2022-06-04 19:50:43 +01:00
CalDescent
66a3322ea6 Bump version to 3.3.3 2022-06-04 19:16:01 +01:00
CalDescent
4965cb7121 Set BlockV2Message min peer version to 3.3.3 2022-06-04 15:50:17 +01:00
CalDescent
b92b1fecb0 disableReferenceTimestamp set to 1655222400000 (Tuesday, 14 June 2022 16:00:00 UTC) 2022-06-04 14:51:37 +01:00
CalDescent
43a75420d0 Merge branch 'disable-reference' 2022-06-04 14:46:51 +01:00
catbref
e85026f866 Initial work on reducing network load for transferring blocks.
Reduced AT state info from per-AT address + state hash + fees to AT count + total AT fees + hash of all AT states.
Modified Block and Controller to support above. Controller needs more work regarding CachedBlockMessages.
Note that blocks fetched from archive are in old V1 format.
Changed Triple<BlockData, List<TransactionData>, List<ATStateData>> to BlockTransformation to support both V1 and V2 forms.

Set min peer version to 3.3.203 in BlockV2Message class.
2022-06-04 14:43:46 +01:00
CalDescent
ba7b9f3ad8 Added feature to allow repository to be kept intact after running certain tests 2022-06-04 14:17:01 +01:00
catbref
4eb58d3591 BlockTimestampTests to show results from changing blockTimingsByHeight 2022-06-04 12:36:36 +01:00
catbref
8d8e58a905 Network$NetworkProcessor now has its own LOGGER 2022-06-04 12:36:36 +01:00
catbref
8f58da4f52 OnlineAccountsManager:
Bump v3 min peer version from 3.2.203 to 3.3.203
No need for toOnlineAccountTimestamp(long) as we only ever use getCurrentOnlineAccountTimestamp().
Latter now returns Long and does the call to NTP.getTime() on behalf of caller, removing duplicated NTP.getTime() calls and null checks in multiple callers.
Add aggregate-signature feature-trigger timestamp threshold checks where needed, near sign() and verify() calls.
Improve logging - but some logging will need to be removed / reduced before merging.
2022-06-04 12:36:36 +01:00
catbref
a4e2aedde1 Remove debug-hindering "final" modifier from effectively final locals 2022-06-04 12:36:36 +01:00
catbref
24d04fe928 Block.mint() always uses latest timestamped online accounts 2022-06-04 12:36:35 +01:00
catbref
0cf32f6c5e BlockMinter now only acquires repository instance as needed to prevent long HSQLDB rollbacks 2022-06-04 12:35:54 +01:00
catbref
84d850ee0b WIP: use blockchain feature-trigger "aggregateSignatureTimestamp" to determine when online-accounts sigs and block sigs switch to aggregate sigs 2022-06-04 12:35:51 +01:00
catbref
51930d3ccf Move some private key methods to Crypto class 2022-06-04 12:35:15 +01:00
catbref
c5e5316f2e Schnorr public key and signature aggregation for 'online accounts'.
Aggregated signature should reduce block payload significantly,
as well as associated network, memory & CPU loads.

org.qortal.crypto.BouncyCastle25519 renamed to Qortal25519Extras.
Our class provides additional features such as DH-based shared secret,
aggregating public keys & signatures and sign/verify for aggregate use.

BouncyCastle's Ed25519 class copied in as BouncyCastleEd25519,
but with 'private' modifiers changed to 'protected',
to allow extension by our Qortal25519Extras class,
and to avoid lots of messy reflection-based calls.
2022-06-04 12:35:15 +01:00
catbref
829ab1eb37 Cherry-pick minor fixes from another branch to resolve "No online accounts - not even our own?" issues 2022-06-04 12:35:03 +01:00
catbref
d9b330b46a OnlineAccountData no longer uses signature in equals() or hashCode() because newer aggregate signatures use random nonces and OAD class doesn't care about / verify sigs 2022-06-04 10:49:59 +01:00
catbref
c032b92d0d Logging fix: size() was called on wrong collection, leading to confusing logging output 2022-06-04 10:49:59 +01:00
catbref
ae92a6eed4 OnlineAccountsV3: slightly rework Block.mint() so it doesn't need to filter so many online accounts
Slight optimization to BlockMinter by adding OnlineAccountsManager.hasOnlineAccounts():boolean instead of returning actual data, only to call isEmpty()!
2022-06-04 10:49:59 +01:00
catbref
712c4463f7 OnlineAccountsV3:
Move online account cache code from Block into OnlineAccountsManager, simplifying Block code and removing duplicated caches from Block also.
This tidies up those remaining set-based getters in OnlineAccountsManager.
No need for currentOnlineAccountsHashes's inner Map to be sorted so addAccounts() creates new ConcurentHashMap insteaad of ConcurrentSkipListMap.

Changed GetOnlineAccountsV3Message to use a single byte for count of hashes as it can only be 1 to 256.
256 is represented by 0.

Comments tidy-up.
Change v3 broadcast interval from 10s to 15s.
2022-06-04 10:49:59 +01:00
catbref
fbdc1e1cdb OnlineAccountsV3:
Adding support for GET_ONLINE_ACCOUNTS_V3 to Controller, which calls OnlineAccountsManager.

With OnlineAccountsV3, instead of nodes sending their list of known online accounts (public keys),
nodes now send a summary which contains hashes of known online accounts, one per timestamp + leading-byte combo.
Thus outgoing messages are much smaller and scale better with more users.
Remote peers compare the hashes and send back lists of online accounts (for that timestamp + leading-byte combo) where hashes do not match.

Massive rewrite of OnlineAccountsManager to maintain online accounts.
Now there are three caches:
1. all online accounts, but split into sets by timestamp
2. 'hashes' of all online accounts, one hash per timestamp+leading-byte combination
Mainly for efficient use by GetOnlineAccountsV3 message constructor.
3. online accounts for the highest blocks on our chain to speed up block processing
Note that highest blocks might be way older than 'current' blocks if we're somewhat behind in syncing.

Other OnlineAccountsManager changes:
* Use scheduling executor service to manage subtasks
* Switch from 'synchronized' to 'concurrent' collections
* Generally switch from Lists to Sets - requires improved OnlineAccountData.hashCode() - further work needed
* Only send V3 messages to peers with version >= 3.2.203 (for testing)
* More info on which online accounts lists are returned depending on use-cases

To test, change your peer's version (in pom.xml?) to v3.2.203.
2022-06-04 10:49:59 +01:00
catbref
f2060fe7a1 Initial work on online-accounts-v3 network messages to drastically reduce network load.
Lots of TODOs to action.
2022-06-04 10:49:59 +01:00
catbref
6950c6bf69 Initial work on reducing network load for transferring blocks.
Reduced AT state info from per-AT address + state hash + fees to AT count + total AT fees + hash of all AT states.
Modified Block and Controller to support above. Controller needs more work regarding CachedBlockMessages.
Note that blocks fetched from archive are in old V1 format.
Changed Triple<BlockData, List<TransactionData>, List<ATStateData>> to BlockTransformation to support both V1 and V2 forms.

Set min peer version to 3.3.203 in BlockV2Message class.
2022-06-04 10:49:11 +01:00
catbref
8a76c6c0de Sync behaviour changes:
* Prefer longer chains
* Compare chain tips
* Skip pre-sync all-peer sorting (for now)
2022-06-04 10:23:31 +01:00
CalDescent
ef51cf5702 Added defensiveness in getOnlineTimestampModulus(), just in case NTP.getTime() returns null 2022-06-02 12:46:11 +01:00
CalDescent
0c3988202e Merge branch 'master' into increase-online-timestamp-modulus 2022-06-02 12:38:05 +01:00
CalDescent
987446cf7f Updated AdvancedInstaller project for v3.3.2 2022-06-02 11:31:56 +01:00
CalDescent
6dd44317c4 Sync pirate wallet every 30 seconds instead of 60, to match behaviour of official wallet. 2022-05-31 10:09:37 +02:00
CalDescent
d2fc705846 Merge remote-tracking branch 'catbref/UnsupportedMessage' 2022-05-31 09:09:08 +02:00
CalDescent
e393150e9c Require references to be the correct length post feature-trigger 2022-05-30 22:40:39 +02:00
CalDescent
43bfd28bcd Revert "Discard unsupported messages instead of disconnecting the peer."
This reverts commit d086ade91f.
2022-05-30 21:46:16 +02:00
catbref
ca8f8a59f4 Better forwards compatibility with newer message types so we don't disconnect newer peers 2022-05-30 20:41:45 +01:00
CalDescent
85a26ae052 Another rework of null seed wallets, to allow them to be saved and loaded.
A full sync is unavoidable for P2SH redeem/refund, so we need to be able to save our progress. Creating a new null seed wallet each time isn't an option because it relies on having a recent checkpoint to avoid having to sync large amounts of blocks every time (sync is per wallet, not per node).
2022-05-30 19:31:55 +02:00
CalDescent
c30b1145a1 Improved ensureSynchronized() as it would often not notice an unsynced wallet. 2022-05-30 18:14:26 +02:00
CalDescent
d086ade91f Discard unsupported messages instead of disconnecting the peer. 2022-05-30 15:27:42 +02:00
CalDescent
64d4c458ec Fixed logging error 2022-05-30 15:16:44 +02:00
CalDescent
2478450694 Revert "Removed "consecutive blocks" limitation in block minter."
This reverts commit f41fbb3b3d.
2022-05-30 13:49:15 +02:00
CalDescent
9f19a042e6 Added test to ensure short (1 byte) references can be imported. 2022-05-30 13:46:00 +02:00
CalDescent
922ffcc0be Modified post-trigger last reference checking, to now require a non-null value
This allows for compatibility with TRANSFER_PRIVS validation in commit 8950bb7, which treats any account with a non-null reference as "existing". It also avoids possible unknown side effects from trying to process and store transactions with a null reference - something that wouldn't have been possible until the validation was removed.
2022-05-30 13:22:15 +02:00
CalDescent
f887fcafe3 Disable last reference validation after feature trigger timestamp (not yet set).
This should prevent the failed transactions that are encountered when issuing two or more in a short space of time. Using a feature trigger (hard fork) to release this, to avoid potential consensus confusion around the time of the update (older versions could consider the main chain invalid until updating).
2022-05-30 13:06:55 +02:00
CalDescent
48b562f71b Auto update check interval slowed from 10s to 30s, to hopefully reduce the chance of encountering "repository in use by another process?" error. 2022-05-28 14:33:37 +02:00
CalDescent
5203742b05 Started work on architecture-specific lite wallet library loading. Paths are not yet correct. 2022-05-28 14:31:38 +02:00
CalDescent
f14b494bfc "Disposable" wallets renamed to "null seed" wallets, as this is a better description of what they are. 2022-05-28 14:28:42 +02:00
CalDescent
9a4ce57001 Increase blockchain lock wait time from 30 to 60 seconds in /transactions/process.
This will hopefully reduce the number of failed tradeoffer listings that result in a nonfunctional tradebot (and subsequent PENDING status shown in the UI)
2022-05-28 13:33:09 +02:00
CalDescent
10af961fdf Consider a node with a block in the last 30 mins to be "up to date" when trading. 2022-05-27 22:31:21 +02:00
CalDescent
33cffe45fd Bump version to 3.3.2 2022-05-27 17:23:53 +02:00
CalDescent
a0ce75a978 Minimum BTC order amount set to 0.001 BTC. Anything lower than that will result in greater than 10% fees. 2022-05-27 16:59:34 +02:00
QuickMythril
8d168f6ad4 Override default Bitcoin trade fee
Reduced to 20 sats/byte.
2022-05-27 06:57:38 -04:00
CalDescent
0875c5bf3b Fix ConcurrentModificationException in getCachedSigValidTransactions() 2022-05-27 10:34:26 +02:00
CalDescent
b17b28d9d6 Catch NoSuchMethodError in ElectrumX, and log it, just in case we ever reencounter a dependency issue. 2022-05-27 10:33:37 +02:00
CalDescent
e95249dc1b Reduced bouncycastle version to 1.69, as 1.70 was having compatibility issues with the ElectrumX code. 2022-05-27 10:30:28 +02:00
CalDescent
bb4bdfede5 Added concept of a "disposable" pirate chain wallet.
This is needed to allow redeem/refund of P2SH without having an actively synced and initialized wallet. It also ultimately avoids us having to retain the wallet entropy in the trade bot states. Various safety checks have been introduced to make sure that a disposable wallet is never used for anything other than P2SH redeem/refund.
2022-05-27 10:29:24 +02:00
CalDescent
e2b241d416 Fix ConcurrentModificationException in getCachedSigValidTransactions() 2022-05-27 10:15:41 +02:00
CalDescent
aeb94fb879 Merge branch 'master' into pirate-chain 2022-05-27 09:16:38 +02:00
QuickMythril
8e71cbd822 Reduce static Bitcoin trade fee 2022-05-26 16:25:07 -04:00
QuickMythril
9896ec2ba6 Fix typo 2022-05-26 15:37:38 -04:00
QuickMythril
9f9a74809e Add Bitcoin ACCTv3
This provides support for restoring BTC in the Trade Portal.
2022-05-26 15:34:51 -04:00
QuickMythril
acce81cdcd Add tray menu item to show Build Version
Core build version in a message dialog for OS which cannot display the entire tooltip.
2022-05-26 15:10:04 -04:00
CalDescent
d72953ae78 Drop expired transactions from the import queue before they are considered "sig valid".
This should prevent expired transactions from being kept alive, adding unnecessary load to the import queue.
2022-05-25 19:06:08 +01:00
CalDescent
32213b1236 Catch all exceptions in PirateLightClient 2022-05-24 20:48:57 +01:00
CalDescent
761d461bad Bump bouncycastle version to 1.70, necessary for ALPN support in some JDKs. 2022-05-24 20:35:19 +01:00
CalDescent
774a3b3dcd Catch RuntimeException, so that the gRPC client is shutdown. 2022-05-24 19:52:23 +01:00
CalDescent
30567d0e87 Correctly handle shutdown of gRPC managed channel on error. 2022-05-24 19:32:41 +01:00
CalDescent
6b53eb5384 Pirate Chain uses the 'b' prefix for P2SH addresses, but the light wallet library is configured to use 't3' (from Zcash), so it's easiest to just derive a different prefix for each destination.
This could be simplified by configuring the light wallet library to use the correct 'b' prefix, but this didn't work when first attempted.
2022-05-23 23:14:54 +01:00
CalDescent
767ef62b64 Added PirateChain.isValidWalletKey() 2022-05-23 22:11:23 +01:00
CalDescent
f7e6d1e5c8 Merge branch 'master' into pirate-chain 2022-05-23 21:55:49 +01:00
CalDescent
551686c2de Updated AdvancedInstaller project for v3.3.1 2022-05-23 21:54:25 +01:00
CalDescent
b73c041cc3 Bump version to 3.3.1 2022-05-23 20:31:36 +01:00
CalDescent
9e8d85285f Removed extra unnecessary digest after writing new data. 2022-05-22 16:13:08 +01:00
CalDescent
f41fbb3b3d Removed "consecutive blocks" limitation in block minter. 2022-05-22 16:13:04 +01:00
CalDescent
3f5240157e Return more detailed errors in trade portal APIs. 2022-05-22 16:11:20 +01:00
CalDescent
7c807f754e First draft of Pirate Chain trade bot. Not tested yet.
This has been modified to a) use full public keys instead of PKH, and b) hand off all transaction building, signing, and broadcasting to the (heavily customized) Pirate light wallet library.
2022-05-22 16:07:48 +01:00
CalDescent
9e1b23caf6 Removed unused constants. 2022-05-22 16:01:12 +01:00
CalDescent
c2bad62d36 Updated status text when not initialized. 2022-05-22 16:00:37 +01:00
CalDescent
4516d44cc0 UnspentOutput additions to support latest PirateChainHTLC methods 2022-05-22 15:55:40 +01:00
CalDescent
9c02b01318 Added PirateChainHTLC.getUnspentFundingTxid(), allowing a minimum amount to be specified.
This will ensure that the correct fundingTxid can be redeemed or refunded by the trade bot.
2022-05-22 15:55:01 +01:00
CalDescent
08fab451d2 Added PirateChainHTLC.getFundingTxid(), to lookup the txid that funded a P2SH. 2022-05-22 15:49:14 +01:00
CalDescent
d47570c642 First draft of Pirate Chain ACCT, with modifications to allow for 33 byte public keys instead of 20 byte PKH. Pirate Chain HTLCs are required to use full public keys rather than hashes. 2022-05-22 12:23:01 +01:00
CalDescent
4547386b1f Increase receiving_account_info column size from 32 to 128 bytes, to allow for Pirate Chain sapling shielded addresses, which are much longer. 2022-05-21 15:24:37 +01:00
CalDescent
ab01dc5e54 Implemented address validation for Pirate Chain 2022-05-21 09:22:49 +01:00
CalDescent
380c742aad Pirate chain minimum order size temporarily decreased to 0.0001, for testing only. 2022-05-21 09:11:24 +01:00
CalDescent
368359917b Use LegacyZcashAddress (copied and modified from bitcoinj) to derive and encode Pirate Chain P2SH addresses.
It wasn't possible to use bitcoinj directly because Zcash-style P2SH addresses have 2 prefix characters (t3), which isn't supported by bitcoinj.
2022-05-20 21:27:38 +01:00
CalDescent
1c1b570cb3 Added Pirate TLC and raw transaction tests 2022-05-20 21:22:18 +01:00
CalDescent
3fe43372a7 Added PirateChainHTLC - uses a different HTLC format suitable for Pirate Chain.
Also removes transaction building code, since this is handled by the light wallet library.
2022-05-20 21:21:51 +01:00
CalDescent
c7bc1d7dcd Hardcode fee to MAINNET_FEE when sending coins. 2022-05-20 21:16:12 +01:00
CalDescent
a7ea6ec80d Added wrapper methods for Pirate LiteWalletJni P2SH funding/redeeming/refunding.
These require a heavily customized version of both piratewallet-light-cli and librustzcash.
2022-05-20 21:14:50 +01:00
CalDescent
9cf574b9e5 Added support for Pirate Chain transaction lookups and deserialization, necessary for HTLC status checks. 2022-05-20 21:07:54 +01:00
CalDescent
20e63a1190 Removed "consecutive blocks" limitation in block minter. 2022-05-20 13:38:56 +01:00
CalDescent
f6fc5de520 Removed extra unnecessary digest after writing new data. 2022-05-20 13:36:56 +01:00
CalDescent
0b89118cd1 Fixed bug in Pirate wallet transaction parsing. 2022-05-20 13:35:52 +01:00
CalDescent
fa3a81575a Reduce wasted time that could otherwise be spent validating queued transaction signatures. 2022-05-14 12:53:36 +01:00
CalDescent
6990766f75 Speed up unconfirmed transaction propagation.
Currently, new transactions take a very long time to be included in each block (or reach the intended recipient), because each node has to obtain a repository lock and import the transaction before it notifies its peers. This can take a long time due to the lock being held by the block minter or synchronizer, and this compounds with every peer that the transaction is routed through.

Validating signatures doesn't require a lock, and so can take place very soon after receipt of a new transaction. This change causes each node to broadcast a new transaction to its peers as soon as its signature is validated, rather than waiting until after the import.

When a notified peer then makes a request for the transaction data itself, this can now be loaded from the sig-valid import queue as an alternative to the repository (since they won't be in the repository until after the import, which likely won't have happened yet).

One small downside to this approach is that each unconfirmed transaction is now notified twice - once after the signature is deemed valid, and again in Controller.onNewTransaction(), but this should be an acceptable trade off given the speed improvements it should achieve. Another downside is that it could cause invalid transactions (with valid signatures) to propagate, but these would quickly be added to each peer's invalidUnconfirmedTransactions list after the import failure, and therefore be ignored.
2022-05-14 12:43:54 +01:00
CalDescent
e1e1a66a0b Improvements and fixes to PirateWallet 2022-05-14 12:10:35 +01:00
CalDescent
e552994f68 Merge branch 'master' into pirate-chain 2022-05-13 13:23:26 +01:00
CalDescent
b33afd99a5 Bump invalid transaction import logging from trace to debug. 2022-05-13 13:22:58 +01:00
CalDescent
3c2ba4a0ea Improved logging when importing transactions. 2022-05-13 12:31:48 +01:00
CalDescent
ab0fc07ee9 Refactored transaction importer, to separate signature validation from importing.
Importing has to be single threaded since it requires the database lock, but there's nothing to stop us from validating signatures on multiple threads, as no lock is required. So it makes sense to separate these two functions to allow for possible multi threaded signature validation in the future, to speed up the process.

Everything remains single threaded in this commit. It should be functionally the same as before, to reduce risk.
2022-05-13 12:31:19 +01:00
CalDescent
001650d48e Fixed typo in currently unused getSignaturesInvolvingAddress() method. 2022-05-13 11:28:55 +01:00
CalDescent
659431ebfd Added "txTypes" parameter to GET /transactions/unconfirmed, to allow optional filtering of unconfirmed transactions by one or more types 2022-05-13 11:28:21 +01:00
CalDescent
0a419cb105 Added "creator" parameter to GET /transactions/unconfirmed, to allow filtering unconfirmed transactions by creator's public key 2022-05-13 11:13:39 +01:00
CalDescent
a4d4d17b82 Added /wallets to .gitignore, in preparation for pirate chain support. 2022-05-12 19:34:53 +01:00
CalDescent
0829ff6908 Fixed bugs in tooltip, introduced in lite node branch 2022-05-12 19:33:12 +01:00
CalDescent
2d1b0fd6d0 Merge branch 'lite-node' 2022-05-12 08:51:05 +01:00
CalDescent
122539596d Merge branch 'qdn-direct-connections' 2022-05-12 08:45:45 +01:00
CalDescent
86015e59a1 Updated AdvancedInstaller project for v3.3.0 2022-05-10 08:27:17 +01:00
CalDescent
107a23f1ec Switch to PirateChainMainNetParams 2022-05-10 08:05:32 +01:00
CalDescent
abce068b97 Catch UnsatisfiedLinkError when initializing Pirate Chain library 2022-05-10 08:05:06 +01:00
CalDescent
28fd9241d4 Fixed issues with merge 2022-05-10 08:01:07 +01:00
CalDescent
3fc4746a52 Merge branch 'master' into pirate-chain
# Conflicts:
#	src/test/java/org/qortal/test/crosschain/DigibyteTests.java
#	src/test/java/org/qortal/test/crosschain/RavencoinTests.java
2022-05-10 07:54:47 +01:00
CalDescent
1ea1e00344 Bump version to 3.3.0 2022-05-09 18:47:19 +01:00
CalDescent
598f219105 Don't require a full synchronization in order to return the ARRR wallet address. 2022-05-08 08:26:00 +01:00
CalDescent
bbf7193c51 Fixed bugs in wallet initialization 2022-05-08 08:24:54 +01:00
CalDescent
adecb21ada Added mainnet xpub addresses for DGB and RVN tests, as testnet support isn't fully implemented yet. 2022-05-07 17:40:36 +01:00
CalDescent
fa4679dcc4 Updated DGB and RVN tests due to conflict. 2022-05-07 17:05:10 +01:00
CalDescent
58917eeeb4 "walletsPath" is now configurable in the settings. 2022-05-07 17:01:19 +01:00
CalDescent
f36e193650 Merge branch 'master' into pirate-chain
# Conflicts:
#	src/main/java/org/qortal/settings/Settings.java
2022-05-07 16:50:32 +01:00
CalDescent
dac484136f Fixed bug in name rebuilding. 2022-05-07 16:46:10 +01:00
CalDescent
999ad857ae Fixed typo 2022-05-07 16:33:10 +01:00
CalDescent
d073b9da65 Added support for Pirate Chain wallets.
Note: this relies on (a modified version of) liblitewallet-jni which is not included, but will ultimately be compiled for each supported architecture and hosted on QDN.

LiteWalletJni code is based on https://github.com/PirateNetwork/cordova-plugin-litewallet - thanks to @CryptoForge for the help in getting this up and running.
2022-05-07 16:32:04 +01:00
CalDescent
aaa0b25106 Make sure to set Peer.isDataPeer() to false as well as true, to prevent bugs due to object reuse.
Also designate a peer as a "data peer" when making an outbound connection to request data from it.
2022-05-02 10:20:23 +01:00
CalDescent
f7dabcaeb0 Increase ONLINE_ACCOUNTS_MODULUS from 5 to 30 mins at a future undecided timestamp.
Note: it's important that this timestamp is set on a 1-hour boundary (such as 16:00:00) to ensure a clean switchover.

# Conflicts:
#	src/main/java/org/qortal/block/BlockChain.java
2022-05-02 08:50:08 +01:00
CalDescent
3409086978 Merge branch 'master' into lite-node 2022-05-02 08:31:43 +01:00
CalDescent
1d7203a6fb Bug fixes found when testing previous commits. 2022-05-01 14:29:24 +01:00
CalDescent
1030b00f0a Keep track of peers requesting data for which we have at least one chunk. Then allow subsequent incoming connections from that peer through, up to a maximum of maxDataPeers.
Direct connections for arbitrary data are currently unlikely to succeed, because those allowing incoming connections generally have their slots maxed out and have reached maxPeers. The idea here is that some connections remain reserved for dedicated arbitrary data transfers, therefore temporarily circumventing the limit (up to a defined maximum number of reserved connections).

Arbitrary data connections will auto disconnect after 2 minutes (we might be able to reduce this at a later date), and it also probably makes sense for the requesting node to disconnect as soon as it has all the chunks that it needs (this part isn't implemented yet).

One downside of this feature is that the listen socket is now going to be accepting connections most of the time, since it is unlikely that we will regularly have 4 data peers connected. This could be improved by modifying the OP_ACCEPT behaviour based on whether we are expecting any data peers to connect. In most cases, this would allow it to remain closed. But for the sake of simplicity I will leave that optimization for a future commit.
2022-05-01 14:02:44 +01:00
CalDescent
0c16d1fc11 Added "maxDataPeerConnectionTime" setting (default 2 mins).
This is used to force a quick disconnect for peers that are only connecting for the purposes of requesting data for a specific arbitrary transaction signature.
2022-05-01 14:02:44 +01:00
CalDescent
ed04375385 Increased default maxPeers from 32 to 36 to compensate - otherwise the network will lose a considerable amount of inbound capacity. 2022-05-01 14:02:44 +01:00
CalDescent
6e49d20383 Added "maxDataPeers" setting to reserve 4 connections by default for direct QDN data requests. 2022-05-01 14:02:44 +01:00
CalDescent
dc34eed203 Include our address when requesting QDN data 2022-05-01 14:02:44 +01:00
CalDescent
e7ee3a06c7 Merge branch 'EPC-fixes' into lite-node 2022-04-30 15:33:07 +01:00
CalDescent
7f9d267992 Improved lite node response error logging. 2022-04-30 15:32:23 +01:00
CalDescent
52904db413 Migrated new lite node message types to new format. 2022-04-30 15:22:50 +01:00
CalDescent
5e0bde226a Merge branch 'EPC-fixes' into lite-node
# Conflicts:
#	src/main/java/org/qortal/network/message/Message.java
2022-04-30 13:25:02 +01:00
CalDescent
348f3c382e Merge branch 'master' into lite-node 2022-04-22 20:40:47 +01:00
CalDescent
1da157d33f Added separate AT serialization tests, based on generic transaction serialization tests. This allows for testing both MESSAGE-type and PAYMENT-type AT transactions. 2022-04-22 20:36:22 +01:00
CalDescent
de4f004a08 Bump to transaction version 6 at a future undecided timestamp. 2022-04-22 20:35:17 +01:00
CalDescent
522ef282c8 Added support for deserialization of MESSAGE-type AT transactions (requires transaction version 6) 2022-04-22 20:34:42 +01:00
CalDescent
b5522ea260 Added support for PAYMENT-type AT transactions in serialization tests 2022-04-22 20:06:37 +01:00
CalDescent
54ff564bb1 Set name for transaction importer thread 2022-04-16 23:32:42 +01:00
CalDescent
f8a5ded0ba Fix for bug introduced in commit cfe9252 2022-04-16 23:20:49 +01:00
CalDescent
a1be66f02b Temporarily ease the filtering of lite node peers, in order to make development easier. 2022-04-16 20:52:31 +01:00
CalDescent
0815ad2cf0 Added AT transaction deserialization, to all them to be sent in messages for lite nodes.
Note that it is currently not easy to distinguish between MESSAGE-type and PAYMENT-type AT transactions, so PAYMENT-type is currently the only one supported (and used). A hard fork will likely be needed in order to specify the type within each message.
2022-04-16 20:52:31 +01:00
CalDescent
3484047ad4 Added GET /transactions/address/{address} API endpoint
This is a more standardized alternative to using GET /transactions/search?address=xyz. This avoids the need to build full transaction search ability into the lite node protocols right away.
2022-04-16 20:52:31 +01:00
CalDescent
a63fa1cce5 Added GET_ACCOUNT_TRANSACTIONS message, as well as a generic TRANSACTIONS message for responses. 2022-04-16 20:52:31 +01:00
CalDescent
59119ebc3b Added GET_NAME message to allow lookups from name to owner (or any other name data). 2022-04-16 20:52:31 +01:00
CalDescent
276f1b7e68 Fixed small errors in earlier commits. 2022-04-16 20:52:31 +01:00
CalDescent
c482e5b5ca Added GET_ACCOUNT_NAMES message to request names for an address, and a generic NAMES message to return a list of NameData objects. The generic NAMES message can be reused for many other responses, such as requesting the various lists of names that the API supports. 2022-04-16 20:52:31 +01:00
CalDescent
8c3e0adf35 Added message types to fetch account details and account balances, and use these in various APIs.
This should bring in enough data for very basic chat and wallet functionality (using addresses rather than registered names).

Data currently comes from a single random peer, however this can be expanded to request from multiple peers to gain confidence in the accuracy of the data. If bad data is returned from a peer, it's not the end of the world since the transaction would just be considered invalid by full nodes and would be thrown out. But this should be mostly avoidable by taking data from multiple sources to improve confidence in its accuracy.
2022-04-16 20:52:30 +01:00
CalDescent
64ff3ac672 Improved comment 2022-04-16 20:52:30 +01:00
CalDescent
cfe92525ed Disable various core functions when running as a lite node.
Lite nodes can't sync or mint blocks, and they also have a very limited ability to verify unconfirmed transactions due to a lack of contextual information (i.e. the blockchain). For now, most validation is skipped and they simply act as relays to help get transactions around the network. Full and topOnly nodes will disregard any invalid transactions upon receipt as usual, and since the lite nodes aren't signing any blocks, there is little risk to the reduced validation, other than the experience of the lite node itself. This can be tightened up considerably as the lite nodes become more powerful, but the current approach works as a PoC.
2022-04-16 20:52:30 +01:00
CalDescent
0e3a9ee2b2 Return the node "type" (full / topOnly / lite) in GET /admin/info endpoint.
This can used by the UI to hide features that aren't supported on lite nodes.
2022-04-16 20:50:27 +01:00
CalDescent
a921db2cc6 Added "lite" setting to designate the core as a lite node. 2022-04-16 20:50:27 +01:00
CalDescent
184984c16f Added ElectrumX equivalent for Pirate Chain, to communicate with a remote lightwalletd
This will most likely be used for by the trade bot, rather than for any wallet functionality.
2022-04-14 20:18:58 +01:00
CalDescent
2cf7a5e114 Generated java gRPC protocol buffers for Pirate Chain network comms.
The command used was:

./protoc --plugin=protoc-gen-grpc-java=/Users/user/Downloads/protoc-gen-grpc-java-1.45.1-osx-x86_64.exe -I=src/main/resources/proto/zcash/ --java_out=src/main/java/ --grpc-java_out=src/main/java/ src/main/resources/proto/zcash/service.proto

Then repeat, replacing service.proto with compact_formats.proto and darkside.proto

Darkside isn't needed for mainnet functionality, but included for completeness, and might be useful for testing.
2022-04-14 20:07:42 +01:00
226 changed files with 42576 additions and 2095 deletions

1
.gitignore vendored
View File

@@ -28,6 +28,7 @@
/WindowsInstaller/Install Files/qortal.jar
/*.7z
/tmp
/wallets
/data*
/src/test/resources/arbitrary/*/.qortal/cache
apikey.txt

View File

@@ -52,14 +52,13 @@
## Single-node testnet
A single-node testnet is possible with code modifications, for basic testing, or to more easily start a new testnet.
To do so, follow these steps:
- Comment out the `if (mintedLastBlock) { }` conditional in BlockMinter.java
- Comment out the `minBlockchainPeers` validation in Settings.validate()
- Set `minBlockchainPeers` to 0 in settings.json
- Set `Synchronizer.RECOVERY_MODE_TIMEOUT` to `0`
- All other steps should remain the same. Only a single reward share key is needed.
- Remember to put these values back after introducing other nodes
A single-node testnet is possible with an additional settings, or to more easily start a new testnet.
Just add this setting:
```
"singleNodeTestnet": true
```
This will automatically allow multiple consecutive blocks to be minted, as well as setting minBlockchainPeers to 0.
Remember to put these values back after introducing other nodes
## Fixed network
@@ -93,3 +92,32 @@ Your options are:
- `qort` tool, but prepend with one-time shell variable: `BASE_URL=some-node-hostname-or-ip:port qort ......`
- `peer-heights`, but use `-t` option, or `BASE_URL` shell variable as above
## Example settings-test.json
```
{
"isTestNet": true,
"bitcoinNet": "TEST3",
"repositoryPath": "db-testnet",
"blockchainConfig": "testchain.json",
"minBlockchainPeers": 1,
"apiDocumentationEnabled": true,
"apiRestricted": false,
"bootstrap": false,
"maxPeerConnectionTime": 999999999,
"localAuthBypassEnabled": true,
"singleNodeTestnet": true,
"recoveryModeTimeout": 0
}
```
## Quick start
Here are some steps to quickly get a single node testnet up and running with a generic minting account:
1. Start with template `settings-test.json`, and create a `testchain.json` based on mainnet's blockchain.json (or obtain one from Qortal developers). These should be in the same directory as the jar.
2. Make sure feature triggers and other timestamp/height activations are correctly set. Generally these would be `0` so that they are enabled from the start.
3. Set a recent genesis `timestamp` in testchain.json, and add this reward share entry:
`{ "type": "REWARD_SHARE", "minterPublicKey": "DwcUnhxjamqppgfXCLgbYRx8H9XFPUc2qYRy3CEvQWEw", "recipient": "QbTDMss7NtRxxQaSqBZtSLSNdSYgvGaqFf", "rewardSharePublicKey": "CRvQXxFfUMfr4q3o1PcUZPA4aPCiubBsXkk47GzRo754", "sharePercent": 0 },`
4. Start the node, passing in settings-test.json, e.g: `java -jar qortal.jar settings-test.json`
5. Once started, add the corresponding minting key to the node:
`curl -X POST "http://localhost:62391/admin/mintingaccounts" -d "F48mYJycFgRdqtc58kiovwbcJgVukjzRE4qRRtRsK9ix"`
6. Alternatively you can use your own minting account instead of the generic one above.
7. After a short while, blocks should be minted from the genesis timestamp until the current time.

View File

@@ -17,10 +17,10 @@
<ROW Property="Manufacturer" Value="Qortal"/>
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
<ROW Property="NTP_GOOD" Value="false"/>
<ROW Property="ProductCode" Value="1033:{8C9CFB9D-BC4C-4142-A5A5-5551BF3B9467} 1049:{4A5BDDD9-ED71-431A-A46F-D19E9DE17216} 2052:{0B9DCE00-BE23-434D-BD6A-1CFA6AB3CA43} 2057:{23D81967-556A-41B8-9981-A739E2820624} " Type="16"/>
<ROW Property="ProductCode" Value="1033:{ADE0C9E9-F7D9-4829-8626-8571C735C4D7} 1049:{F5230C0A-9D8C-4C70-AC72-17CECC8273B8} 2052:{D5A0760C-E5B3-4C4C-97B0-81CC445F07B9} 2057:{EF5EF0BE-0B00-4F5C-A2A0-DF2CB82FF20D} " Type="16"/>
<ROW Property="ProductLanguage" Value="2057"/>
<ROW Property="ProductName" Value="Qortal"/>
<ROW Property="ProductVersion" Value="3.2.5" Type="32"/>
<ROW Property="ProductVersion" Value="3.6.3" Type="32"/>
<ROW Property="RECONFIG_NTP" Value="true"/>
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
@@ -212,7 +212,7 @@
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
<ROW Component="AI_CustomARPName" ComponentId="{D5A1DC7D-914F-4425-8BA6-A1AE05D0F361}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_CustomARPName" ComponentId="{F4F774B9-18DC-4740-9552-EA16B98801C9}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
@@ -1173,7 +1173,7 @@
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
<ROW Action="AI_SetPermissions" Type="11265" Source="userAccounts.dll" Target="OnSetPermissions" WithoutSeq="true"/>
<ROW Action="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(appDir + &quot;log4j2.properties&quot;, appDir + &quot;log4j2-orig.properties&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite %AppDir%\log4j2.properties to update logfile storage path&#13;&#10;var fin = fso.OpenTextFile(appDir + &quot;log4j2-orig.properties&quot;, ForReading, false); // no create&#13;&#10;var fout = fso.OpenTextFile(appDir + &quot;log4j2.properties&quot;, ForWriting, true); // can create&#13;&#10;&#13;&#10;// Copy lines with rewriting where necessary&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;var line = fin.ReadLine();&#13;&#10;&#13;&#10;&#9;var start = line.indexOf(&quot;property.dirname&quot;);&#13;&#10;&#9;if (start &gt; 0) {&#13;&#10;&#9;&#9;// line: # property.dirname = ...appdata...&#13;&#10;&#9;&#9;// uncomment/replace this line for Windows&#13;&#10;&#9;&#9;fout.WriteLine( &quot;property.dirname = &quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) );&#13;&#10;&#9;} else {&#13;&#10;&#9;&#9;// not found - output verbatim&#13;&#10;&#9;&#9;fout.WriteLine( line );&#13;&#10;&#9;}&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_4"/>
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder&#13;&#10;var fts = fso.OpenTextFile(appDir + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;fts.WriteLine( &quot;{&quot; );&#13;&#10;// We need to escape Windows path backslashes to keep JSON valid&#13;&#10;fts.WriteLine( &quot; \&quot;userPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;\&quot;&quot; );&#13;&#10;fts.WriteLine( &quot;}&quot; );&#13;&#10;&#13;&#10;fts.Close();&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(dataFolder + &quot;settings.json&quot;, dataFolder + &quot;settings-orig.json&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite settings.json to update repository path&#13;&#10;var fin = fso.OpenTextFile(dataFolder + &quot;settings-orig.json&quot;, ForReading, false);&#13;&#10;var fout = fso.OpenTextFile(dataFolder + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;// First line should contain opening brace&#13;&#10;fout.WriteLine( fin.ReadLine() );&#13;&#10;&#13;&#10;// Append our entries&#13;&#10;fout.WriteLine( &quot; \&quot;repositoryPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;db\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;dataPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;data\&quot;,&quot; );&#13;&#10;&#13;&#10;// copy rest of settings&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;fout.WriteLine( fin.ReadLine() );&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_3"/>
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property(&quot;CustomActionData&quot;);&#13;&#10;var actionDataArray = actionData.split(&quot;|&quot;);&#13;&#10;var appDir = actionDataArray[0];&#13;&#10;var dataFolder = actionDataArray[1] + actionDataArray[2] + &quot;\\&quot;;&#13;&#10;&#13;&#10;var ForReading = 1, ForWriting = 2, ForAppending = 8;&#13;&#10;var fso = new ActiveXObject(&quot;Scripting.FileSystemObject&quot;);&#13;&#10;&#13;&#10;// Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder&#13;&#10;var fts = fso.OpenTextFile(appDir + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;fts.WriteLine( &quot;{&quot; );&#13;&#10;// We need to escape Windows path backslashes to keep JSON valid&#13;&#10;fts.WriteLine( &quot; \&quot;userPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;\&quot;&quot; );&#13;&#10;fts.WriteLine( &quot;}&quot; );&#13;&#10;&#13;&#10;fts.Close();&#13;&#10;&#13;&#10;// Make copy&#13;&#10;fso.CopyFile(dataFolder + &quot;settings.json&quot;, dataFolder + &quot;settings-orig.json&quot;, true); // overwrite&#13;&#10;&#13;&#10;// Rewrite settings.json to update repository path&#13;&#10;var fin = fso.OpenTextFile(dataFolder + &quot;settings-orig.json&quot;, ForReading, false);&#13;&#10;var fout = fso.OpenTextFile(dataFolder + &quot;settings.json&quot;, ForWriting, true);&#13;&#10;&#13;&#10;// First line should contain opening brace&#13;&#10;fout.WriteLine( fin.ReadLine() );&#13;&#10;&#13;&#10;// Append our entries&#13;&#10;fout.WriteLine( &quot; \&quot;repositoryPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;db\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;dataPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;data\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;walletsPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;wallets\&quot;,&quot; );&#13;&#10;fout.WriteLine( &quot; \&quot;listsPath\&quot;: \&quot;&quot; + dataFolder.split(&apos;\\&apos;).join(&apos;\\\\&apos;) + &quot;lists\&quot;,&quot; );&#13;&#10;&#13;&#10;// copy rest of settings&#13;&#10;while( !fin.AtEndOfStream ) {&#13;&#10;&#9;fout.WriteLine( fin.ReadLine() );&#13;&#10;}&#13;&#10;&#13;&#10;fin.Close();&#13;&#10;fout.Close();&#13;&#10;" AdditionalSeq="AI_DATA_SETTER_3"/>
<ROW Action="DetectRunningProcess" Type="1" Source="aicustact.dll" Target="DetectProcess" Options="3" AdditionalSeq="AI_DATA_SETTER_8"/>
<ROW Action="DetectW32Time" Type="1" Source="aicustact.dll" Target="DetectService" Options="3" AdditionalSeq="AI_DATA_SETTER_11"/>
<ROW Action="NTP_config" Type="3090" Source="ntpcfg.bat"/>

Binary file not shown.

View File

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

View File

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

30
pom.xml
View File

@@ -3,15 +3,15 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>3.2.5</version>
<version>3.7.0</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>
<altcoinj.version>6628cfd</altcoinj.version>
<altcoinj.version>7dc8c6f</altcoinj.version>
<bitcoinj.version>0.15.10</bitcoinj.version>
<bouncycastle.version>1.64</bouncycastle.version>
<bouncycastle.version>1.69</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.3.8</ciyam-at.version>
<ciyam-at.version>1.4.0</ciyam-at.version>
<commons-net.version>3.6</commons-net.version>
<commons-text.version>1.8</commons-text.version>
<commons-io.version>2.6</commons-io.version>
@@ -34,6 +34,8 @@
<package-info-maven-plugin.version>1.1.0</package-info-maven-plugin.version>
<jsoup.version>1.13.1</jsoup.version>
<java-diff-utils.version>4.10</java-diff-utils.version>
<grpc.version>1.45.1</grpc.version>
<protobuf.version>3.19.4</protobuf.version>
</properties>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
@@ -705,5 +707,25 @@
<artifactId>java-diff-utils</artifactId>
<version>${java-diff-utils.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
</dependencies>
</project>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* LiteWalletJni code based on https://github.com/PirateNetwork/cordova-plugin-litewallet
*
* MIT License
*
* Copyright (c) 2020 Zero Currency Coin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.rust.litewalletjni;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.controller.PirateChainWalletController;
import java.nio.file.Path;
import java.nio.file.Paths;
public class LiteWalletJni {
protected static final Logger LOGGER = LogManager.getLogger(LiteWalletJni.class);
public static native String initlogging();
public static native String initnew(final String serveruri, final String params, final String saplingOutputb64, final String saplingSpendb64);
public static native String initfromseed(final String serveruri, final String params, final String seed, final String birthday, final String saplingOutputb64, final String saplingSpendb64);
public static native String initfromb64(final String serveruri, final String params, final String datab64, final String saplingOutputb64, final String saplingSpendb64);
public static native String save();
public static native String execute(final String cmd, final String args);
public static native String getseedphrase();
public static native String getseedphrasefromentropyb64(final String entropy64);
public static native String checkseedphrase(final String input);
private static boolean loaded = false;
public static void loadLibrary() {
if (loaded) {
return;
}
String osName = System.getProperty("os.name");
String osArchitecture = System.getProperty("os.arch");
LOGGER.info("OS Name: {}", osName);
LOGGER.info("OS Architecture: {}", osArchitecture);
try {
String libFileName = PirateChainWalletController.getRustLibFilename();
if (libFileName == null) {
LOGGER.info("Library not found for OS: {}, arch: {}", osName, osArchitecture);
return;
}
Path libPath = Paths.get(PirateChainWalletController.getRustLibOuterDirectory().toString(), libFileName);
System.load(libPath.toAbsolutePath().toString());
loaded = true;
}
catch (UnsatisfiedLinkError e) {
LOGGER.info("Unable to load library");
}
}
public static boolean isLoaded() {
return loaded;
}
}

View File

@@ -8,6 +8,7 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.Security;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -18,6 +19,8 @@ import org.qortal.api.ApiRequest;
import org.qortal.controller.AutoUpdate;
import org.qortal.settings.Settings;
import static org.qortal.controller.AutoUpdate.AGENTLIB_JVM_HOLDER_ARG;
public class ApplyUpdate {
static {
@@ -37,7 +40,7 @@ public class ApplyUpdate {
private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS";
private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4";
private static final long CHECK_INTERVAL = 10 * 1000L; // ms
private static final long CHECK_INTERVAL = 30 * 1000L; // ms
private static final int MAX_ATTEMPTS = 12;
public static void main(String[] args) {
@@ -197,6 +200,11 @@ public class ApplyUpdate {
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Reapply any retained, but disabled, -agentlib JVM arg
javaCmd = javaCmd.stream()
.map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib"))
.collect(Collectors.toList());
// Call mainClass in JAR
javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME));
@@ -205,7 +213,7 @@ public class ApplyUpdate {
}
try {
LOGGER.info(() -> String.format("Restarting node with: %s", String.join(" ", javaCmd)));
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
@@ -214,8 +222,15 @@ public class ApplyUpdate {
processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE);
}
processBuilder.start();
} catch (IOException e) {
// New process will inherit our stdout and stderr
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
// Nothing to pipe to new process, so close output stream (process's stdin)
process.getOutputStream().close();
} catch (Exception e) {
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
}
}

View File

@@ -8,11 +8,13 @@ import javax.xml.bind.annotation.XmlAccessorType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.block.BlockChain;
import org.qortal.controller.LiteNode;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.RewardShareData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
@@ -59,7 +61,17 @@ public class Account {
// Balance manipulations - assetId is 0 for QORT
public long getConfirmedBalance(long assetId) throws DataException {
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
AccountBalanceData accountBalanceData;
if (Settings.getInstance().isLite()) {
// Lite nodes request data from peers instead of the local db
accountBalanceData = LiteNode.getInstance().fetchAccountBalance(this.address, assetId);
}
else {
// All other node types fetch from the local db
accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
}
if (accountBalanceData == null)
return 0;

View File

@@ -11,15 +11,15 @@ public class PrivateKeyAccount extends PublicKeyAccount {
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
/**
* Create PrivateKeyAccount using byte[32] seed.
* Create PrivateKeyAccount using byte[32] private key.
*
* @param seed
* @param privateKey
* byte[32] used to create private/public key pair
* @throws IllegalArgumentException
* if passed invalid seed
* if passed invalid privateKey
*/
public PrivateKeyAccount(Repository repository, byte[] seed) {
this(repository, new Ed25519PrivateKeyParameters(seed, 0));
public PrivateKeyAccount(Repository repository, byte[] privateKey) {
this(repository, new Ed25519PrivateKeyParameters(privateKey, 0));
}
private PrivateKeyAccount(Repository repository, Ed25519PrivateKeyParameters edPrivateKeyParams) {
@@ -37,10 +37,6 @@ public class PrivateKeyAccount extends PublicKeyAccount {
return this.privateKey;
}
public static byte[] toPublicKey(byte[] seed) {
return new Ed25519PrivateKeyParameters(seed, 0).generatePublicKey().getEncoded();
}
public byte[] sign(byte[] message) {
return Crypto.sign(this.edPrivateKeyParams, message);
}

View File

@@ -1,7 +1,7 @@
package org.qortal.api.model;
import io.swagger.v3.oas.annotations.media.Schema;
import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.network.Handshake;
import org.qortal.network.Peer;
@@ -63,11 +63,11 @@ public class ConnectedPeer {
this.age = "connecting...";
}
PeerChainTipData peerChainTipData = peer.getChainTipData();
BlockSummaryData peerChainTipData = peer.getChainTipData();
if (peerChainTipData != null) {
this.lastHeight = peerChainTipData.getLastHeight();
this.lastBlockSignature = peerChainTipData.getLastBlockSignature();
this.lastBlockTimestamp = peerChainTipData.getLastBlockTimestamp();
this.lastHeight = peerChainTipData.getHeight();
this.lastBlockSignature = peerChainTipData.getSignature();
this.lastBlockTimestamp = peerChainTipData.getTimestamp();
}
}

View File

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

View File

@@ -4,6 +4,7 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.controller.Controller;
import org.qortal.controller.OnlineAccountsManager;
import org.qortal.controller.Synchronizer;
import org.qortal.network.Network;
@@ -21,7 +22,7 @@ public class NodeStatus {
public final int height;
public NodeStatus() {
this.isMintingPossible = Controller.getInstance().isMintingPossible();
this.isMintingPossible = OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures();
this.syncPercent = Synchronizer.getInstance().getSyncPercent();
this.isSynchronizing = Synchronizer.getInstance().isSynchronizing();

View File

@@ -0,0 +1,32 @@
package org.qortal.api.model.crosschain;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlAccessorType(XmlAccessType.FIELD)
public class PirateChainSendRequest {
@Schema(description = "32 bytes of entropy, Base58 encoded", example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV")
public String entropy58;
@Schema(description = "Recipient's Pirate Chain address", example = "zc...")
public String receivingAddress;
@Schema(description = "Amount of ARRR to send", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long arrrAmount;
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 ARRR (100 sats) per byte", example = "0.00000100", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public Long feePerByte;
@Schema(description = "Optional memo to include information for the recipient", example = "zc...")
public String memo;
public PirateChainSendRequest() {
}
}

View File

@@ -30,6 +30,7 @@ import org.qortal.api.Security;
import org.qortal.api.model.ApiOnlineAccount;
import org.qortal.api.model.RewardShareKeyRequest;
import org.qortal.asset.Asset;
import org.qortal.controller.LiteNode;
import org.qortal.controller.OnlineAccountsManager;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
@@ -109,18 +110,26 @@ public class AddressesResource {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
AccountData accountData;
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
lastReference = accountData.getReference();
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
if (Settings.getInstance().isLite()) {
// Lite nodes request data from peers instead of the local db
accountData = LiteNode.getInstance().fetchAccountData(address);
}
else {
// All other node types request data from local db
try (final Repository repository = RepositoryManager.getRepository()) {
accountData = repository.getAccountRepository().getAccount(address);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
byte[] lastReference = accountData.getReference();
if (lastReference == null || lastReference.length == 0)
return "false";
@@ -196,6 +205,10 @@ public class AddressesResource {
try (final Repository repository = RepositoryManager.getRepository()) {
List<OnlineAccountLevel> onlineAccountLevels = new ArrayList<>();
// Prepopulate all levels
for (int i=0; i<=10; i++)
onlineAccountLevels.add(new OnlineAccountLevel(i, 0));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
try {
final int minterLevel = Account.getRewardShareEffectiveMintingLevelIncludingLevelZero(repository, onlineAccountData.getPublicKey());

View File

@@ -119,10 +119,23 @@ public class AdminResource {
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
nodeInfo.type = getNodeType();
return nodeInfo;
}
private String getNodeType() {
if (Settings.getInstance().isLite()) {
return "lite";
}
else if (Settings.getInstance().isTopOnly()) {
return "topOnly";
}
else {
return "full";
}
}
@GET
@Path("/status")
@Operation(
@@ -715,6 +728,49 @@ public class AdminResource {
}
}
@POST
@Path("/repository/importarchivedtrades")
@Operation(
summary = "Imports archived trades from TradeBotStatesArchive.json",
description = "This can be used to recover trades that exist in the archive only, which may be needed if a<br />" +
"problem occurred during the proof-of-work computation stage of a buy request.",
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
@SecurityRequirement(name = "apiKey")
public boolean importArchivedTrades(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
blockchainLock.lockInterruptibly();
try {
repository.importDataFromFile("qortal-backup/TradeBotStatesArchive.json");
repository.saveChanges();
return true;
} catch (IOException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
} finally {
blockchainLock.unlock();
}
} catch (InterruptedException e) {
// We couldn't lock blockchain to perform import
return false;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/apikey/generate")

View File

@@ -12,10 +12,10 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.ServletContext;
@@ -45,6 +45,7 @@ import org.qortal.data.arbitrary.*;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.list.ResourceListManager;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -56,7 +57,9 @@ import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.ArbitraryTransactionUtils;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import org.qortal.utils.ZipUtils;
@Path("/arbitrary")
@@ -90,6 +93,7 @@ public class ArbitraryResource {
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
@Parameter(description = "Filter names by list") @QueryParam("namefilter") String nameFilter,
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
@@ -106,8 +110,18 @@ public class ArbitraryResource {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "identifier cannot be specified when requesting a default resource");
}
// Load filter from list if needed
List<String> names = null;
if (nameFilter != null) {
names = ResourceListManager.getInstance().getStringsInList(nameFilter);
if (names.isEmpty()) {
// List doesn't exist or is empty - so there will be no matches
return new ArrayList<>();
}
}
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
.getArbitraryResources(service, identifier, null, defaultRes, limit, offset, reverse);
.getArbitraryResources(service, identifier, names, defaultRes, limit, offset, reverse);
if (resources == null) {
return new ArrayList<>();
@@ -215,7 +229,7 @@ public class ArbitraryResource {
String name = creatorName.name;
if (name != null) {
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
.getArbitraryResources(service, identifier, name, defaultRes, null, null, reverse);
.getArbitraryResources(service, identifier, Arrays.asList(name), defaultRes, null, null, reverse);
if (includeStatus != null && includeStatus) {
resources = this.addStatusToResources(resources);
@@ -253,7 +267,7 @@ public class ArbitraryResource {
@QueryParam("build") Boolean build) {
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
return this.getStatus(service, name, null, build);
return ArbitraryTransactionUtils.getStatus(service, name, null, build);
}
@GET
@@ -275,7 +289,7 @@ public class ArbitraryResource {
@QueryParam("build") Boolean build) {
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
return this.getStatus(service, name, identifier, build);
return ArbitraryTransactionUtils.getStatus(service, name, identifier, build);
}
@@ -1099,7 +1113,8 @@ public class ArbitraryResource {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, error);
}
if (!Controller.getInstance().isUpToDate()) {
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
}
@@ -1245,24 +1260,6 @@ public class ArbitraryResource {
}
private ArbitraryResourceStatus getStatus(Service service, String name, String identifier, Boolean build) {
// If "build=true" has been specified in the query string, build the resource before returning its status
if (build != null && build == true) {
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
try {
if (!reader.isBuilding()) {
reader.loadSynchronously(false);
}
} catch (Exception e) {
// No need to handle exception, as it will be reflected in the status
}
}
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
return resource.getStatus(false);
}
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
// Determine and add the status of each resource
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();

View File

@@ -114,7 +114,7 @@ public class BlocksResource {
@Path("/signature/{signature}/data")
@Operation(
summary = "Fetch serialized, base58 encoded block data using base58 signature",
description = "Returns serialized data for the block that matches the given signature",
description = "Returns serialized data for the block that matches the given signature, and an optional block serialization version",
responses = {
@ApiResponse(
description = "the block data",
@@ -125,7 +125,7 @@ public class BlocksResource {
@ApiErrors({
ApiError.INVALID_SIGNATURE, ApiError.BLOCK_UNKNOWN, ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE
})
public String getSerializedBlockData(@PathParam("signature") String signature58) {
public String getSerializedBlockData(@PathParam("signature") String signature58, @QueryParam("version") Integer version) {
// Decode signature
byte[] signature;
try {
@@ -136,20 +136,41 @@ public class BlocksResource {
try (final Repository repository = RepositoryManager.getRepository()) {
// Default to version 1
if (version == null) {
version = 1;
}
// Check the database first
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData != null) {
Block block = new Block(repository, blockData);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
bytes.write(BlockTransformer.toBytes(block));
switch (version) {
case 1:
bytes.write(BlockTransformer.toBytes(block));
break;
case 2:
bytes.write(BlockTransformer.toBytesV2(block));
break;
default:
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
return Base58.encode(bytes.toByteArray());
}
// Not found, so try the block archive
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
if (bytes != null) {
return Base58.encode(bytes);
if (version != 1) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Archived blocks require version 1");
}
return Base58.encode(bytes);
}
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);

View File

@@ -69,6 +69,9 @@ public class ChatResource {
public List<ChatMessage> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
@QueryParam("txGroupId") Integer txGroupId,
@QueryParam("involving") List<String> involvingAddresses,
@QueryParam("reference") String reference,
@QueryParam("chatreference") String chatReference,
@QueryParam("haschatreference") Boolean hasChatReference,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
@@ -87,11 +90,22 @@ public class ChatResource {
if (after != null && after < 1500000000000L)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] referenceBytes = null;
if (reference != null)
referenceBytes = Base58.decode(reference);
byte[] chatReferenceBytes = null;
if (chatReference != null)
chatReferenceBytes = Base58.decode(chatReference);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getMessagesMatchingCriteria(
before,
after,
txGroupId,
referenceBytes,
chatReferenceBytes,
hasChatReference,
involvingAddresses,
limit, offset, reverse);
} catch (DataException e) {
@@ -99,6 +113,38 @@ public class ChatResource {
}
}
@GET
@Path("/message/{signature}")
@Operation(
summary = "Find chat message by signature",
responses = {
@ApiResponse(
description = "CHAT message",
content = @Content(
schema = @Schema(
implementation = ChatMessage.class
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public ChatMessage getMessageBySignature(@PathParam("signature") String signature58) {
byte[] signature = Base58.decode(signature58);
try (final Repository repository = RepositoryManager.getRepository()) {
ChatTransactionData chatTransactionData = (ChatTransactionData) repository.getTransactionRepository().fromSignature(signature);
if (chatTransactionData == null) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Message not found");
}
return repository.getChatRepository().toChatMessage(chatTransactionData);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/active/{address}")
@Operation(

View File

@@ -1,5 +1,6 @@
package org.qortal.api.resource;
import com.google.common.hash.HashCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -7,8 +8,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
@@ -21,6 +25,7 @@ import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.qortal.api.*;
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
import org.qortal.controller.Controller;
import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
@@ -284,6 +289,12 @@ public class CrossChainHtlcResource {
continue;
}
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
LOGGER.info("Skipping AT {} because ARRR is currently unsupported", atAddress);
continue;
}
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
if (crossChainTradeData == null) {
LOGGER.info("Couldn't find crosschain trade data for AT {}", atAddress);
@@ -363,10 +374,6 @@ public class CrossChainHtlcResource {
// Use secret-A to redeem P2SH-A
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
if (bitcoiny.getClass() == Bitcoin.class) {
LOGGER.info("Redeeming a Bitcoin HTLC is not yet supported");
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
int lockTime = crossChainTradeData.lockTimeA;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
@@ -574,7 +581,7 @@ public class CrossChainHtlcResource {
// If the AT is "finished" then it will have a zero balance
// In these cases we should avoid HTLC refunds if tbe QORT haven't been returned to the seller
if (atData.getIsFinished() && crossChainTradeData.mode != AcctMode.REFUNDED && crossChainTradeData.mode != AcctMode.CANCELLED) {
LOGGER.info(String.format("Skipping AT %s because the QORT has already been redemed", atAddress));
LOGGER.info(String.format("Skipping AT %s because the QORT has already been redeemed by the buyer", atAddress));
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
@@ -584,11 +591,6 @@ public class CrossChainHtlcResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
if (bitcoiny.getClass() == Bitcoin.class) {
LOGGER.info("Refunding a Bitcoin HTLC is not yet supported");
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
int lockTime = tradeBotData.getLockTimeA();
// We can't refund P2SH-A until lockTime-A has passed
@@ -600,15 +602,26 @@ public class CrossChainHtlcResource {
if (medianBlockTime <= lockTime)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
long p2shFee = bitcoiny.getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
// Create redeem script based on destination chain
byte[] redeemScriptA;
String p2shAddressA;
BitcoinyHTLC.Status htlcStatusA;
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA);
htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
}
else {
redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
}
LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
switch (htlcStatusA) {
case UNFUNDED:
@@ -625,18 +638,45 @@ public class CrossChainHtlcResource {
case FUNDED:{
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
// Validate the destination foreign blockchain address
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
// Pirate Chain custom integration
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
PirateChain pirateChain = PirateChain.getInstance();
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA);
// Get funding txid
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA);
if (fundingTxidHex == null) {
throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
}
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
byte[] privateKey = tradeBotData.getTradePrivateKey();
String privateKey58 = Base58.encode(privateKey);
String redeemScript58 = Base58.encode(redeemScriptA);
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58);
LOGGER.info("Refund txid: {}", txid);
}
else {
// ElectrumX coins
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
// Validate the destination foreign blockchain address
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
bitcoiny.broadcastTransaction(p2shRefundTransaction);
}
bitcoiny.broadcastTransaction(p2shRefundTransaction);
return true;
}
}

View File

@@ -0,0 +1,229 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.crosschain.PirateChainSendRequest;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.PirateChain;
import org.qortal.crosschain.SimpleTransaction;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.util.List;
@Path("/crosschain/arrr")
@Tag(name = "Cross-Chain (Pirate Chain)")
public class CrossChainPirateChainResource {
@Context
HttpServletRequest request;
@POST
@Path("/walletbalance")
@Operation(
summary = "Returns ARRR balance",
description = "Supply 32 bytes of entropy, Base58 encoded",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "32 bytes of entropy, Base58 encoded",
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getPirateChainWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
Security.checkApiCallAllowed(request);
PirateChain pirateChain = PirateChain.getInstance();
try {
Long balance = pirateChain.getWalletBalance(entropy58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return balance.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
}
}
@POST
@Path("/wallettransactions")
@Operation(
summary = "Returns transactions",
description = "Supply 32 bytes of entropy, Base58 encoded",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "32 bytes of entropy, Base58 encoded",
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<SimpleTransaction> getPirateChainWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
Security.checkApiCallAllowed(request);
PirateChain pirateChain = PirateChain.getInstance();
try {
return pirateChain.getWalletTransactions(entropy58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
}
}
@POST
@Path("/send")
@Operation(
summary = "Sends ARRR from wallet",
description = "Currently supports 'legacy' P2PKH PirateChain addresses and Native SegWit (P2WPKH) addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "32 bytes of entropy, Base58 encoded",
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String sendBitcoin(@HeaderParam(Security.API_KEY_HEADER) String apiKey, PirateChainSendRequest pirateChainSendRequest) {
Security.checkApiCallAllowed(request);
if (pirateChainSendRequest.arrrAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (pirateChainSendRequest.feePerByte != null && pirateChainSendRequest.feePerByte <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
PirateChain pirateChain = PirateChain.getInstance();
try {
return pirateChain.sendCoins(pirateChainSendRequest);
} catch (ForeignBlockchainException e) {
// TODO
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
}
}
@POST
@Path("/walletaddress")
@Operation(
summary = "Returns main wallet address",
description = "Supply 32 bytes of entropy, Base58 encoded",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "32 bytes of entropy, Base58 encoded",
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getPirateChainWalletAddress(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
Security.checkApiCallAllowed(request);
PirateChain pirateChain = PirateChain.getInstance();
try {
return pirateChain.getWalletAddress(entropy58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
}
}
@POST
@Path("/syncstatus")
@Operation(
summary = "Returns synchronization status",
description = "Supply 32 bytes of entropy, Base58 encoded",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "32 bytes of entropy, Base58 encoded",
example = "5oSXF53qENtdUyKhqSxYzP57m6RhVFP9BJKRr9E5kRGV"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getPirateChainSyncStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String entropy58) {
Security.checkApiCallAllowed(request);
PirateChain pirateChain = PirateChain.getInstance();
try {
return pirateChain.getSyncStatus(entropy58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
}
}
}

View File

@@ -42,6 +42,7 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@Path("/crosschain/tradebot")
@Tag(name = "Cross-Chain (Trade-Bot)")
@@ -137,7 +138,8 @@ public class CrossChainTradeBotResource {
if (tradeBotCreateRequest.qortAmount <= 0 || tradeBotCreateRequest.fundingQortAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_SIZE_TOO_SMALL);
if (!Controller.getInstance().isUpToDate())
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -153,7 +155,7 @@ public class CrossChainTradeBotResource {
return Base58.encode(unsignedBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
}
}
@@ -198,7 +200,8 @@ public class CrossChainTradeBotResource {
if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (!Controller.getInstance().isUpToDate())
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
// Extract data from cross-chain trading AT
@@ -237,7 +240,7 @@ public class CrossChainTradeBotResource {
return "false";
}
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
}
}

View File

@@ -26,6 +26,7 @@ import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.NameSummary;
import org.qortal.controller.LiteNode;
import org.qortal.crypto.Crypto;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.BuyNameTransactionData;
@@ -101,7 +102,14 @@ public class NamesResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
List<NameData> names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse);
List<NameData> names;
if (Settings.getInstance().isLite()) {
names = LiteNode.getInstance().fetchAccountNames(address);
}
else {
names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse);
}
return names.stream().map(NameSummary::new).collect(Collectors.toList());
} catch (DataException e) {
@@ -126,10 +134,18 @@ public class NamesResource {
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public NameData getName(@PathParam("name") String name) {
try (final Repository repository = RepositoryManager.getRepository()) {
NameData nameData = repository.getNameRepository().fromName(name);
NameData nameData;
if (nameData == null)
if (Settings.getInstance().isLite()) {
nameData = LiteNode.getInstance().fetchNameData(name);
}
else {
nameData = repository.getNameRepository().fromName(name);
}
if (nameData == null) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NAME_UNKNOWN);
}
return nameData;
} catch (ApiException e) {

View File

@@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@@ -32,6 +33,8 @@ import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.SimpleTransactionSignRequest;
import org.qortal.controller.Controller;
import org.qortal.controller.LiteNode;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.TransactionData;
import org.qortal.globalization.Translator;
import org.qortal.repository.DataException;
@@ -250,14 +253,29 @@ public class TransactionsResource {
ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> getUnconfirmedTransactions(@Parameter(
description = "A list of transaction types"
) @QueryParam("txType") List<TransactionType> txTypes, @Parameter(
description = "Transaction creator's base58 encoded public key"
) @QueryParam("creator") String creatorPublicKey58, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
// Decode public key if supplied
byte[] creatorPublicKey = null;
if (creatorPublicKey58 != null) {
try {
creatorPublicKey = Base58.decode(creatorPublicKey58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
}
}
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getTransactionRepository().getUnconfirmedTransactions(limit, offset, reverse);
return repository.getTransactionRepository().getUnconfirmedTransactions(txTypes, creatorPublicKey, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@@ -366,6 +384,73 @@ public class TransactionsResource {
}
}
@GET
@Path("/address/{address}")
@Operation(
summary = "Returns transactions for given address",
responses = {
@ApiResponse(
description = "transactions",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionData.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<TransactionData> getAddressTransactions(@PathParam("address") String address,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
if (!Crypto.isValidAddress(address)) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
}
if (limit == null) {
limit = 0;
}
if (offset == null) {
offset = 0;
}
List<TransactionData> transactions;
if (Settings.getInstance().isLite()) {
// Fetch from network
transactions = LiteNode.getInstance().fetchAccountTransactions(address, limit, offset);
// Sort the data, since we can't guarantee the order that a peer sent it in
if (reverse) {
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp).reversed());
} else {
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp));
}
}
else {
// Fetch from local db
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null,
null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, reverse);
// Expand signatures to transactions
transactions = new ArrayList<>(signatures.size());
for (byte[] signature : signatures) {
transactions.add(repository.getTransactionRepository().fromSignature(signature));
}
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
return transactions;
}
@GET
@Path("/unitfee")
@Operation(
@@ -638,9 +723,9 @@ public class TransactionsResource {
ApiError.BLOCKCHAIN_NEEDS_SYNC, ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE
})
public String processTransaction(String rawBytes58) {
// Only allow a transaction to be processed if our latest block is less than 30 minutes old
// Only allow a transaction to be processed if our latest block is less than 60 minutes old
// If older than this, we should first wait until the blockchain is synced
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
@@ -663,7 +748,7 @@ public class TransactionsResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS))
if (!blockchainLock.tryLock(60, TimeUnit.SECONDS))
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
try {

View File

@@ -46,6 +46,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
null,
txGroupId,
null,
null,
null,
null,
null, null, null);
sendMessages(session, chatMessages);
@@ -69,6 +72,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
try (final Repository repository = RepositoryManager.getRepository()) {
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
null,
null,
null,
null,
null,
null,

View File

@@ -2,10 +2,7 @@ package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
@@ -85,6 +82,7 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean excludeInitialData = queryParams.get("excludeInitialData") != null;
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
@@ -98,15 +96,22 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener {
// save session's preferred blockchain (if any)
sessionBlockchain.put(session, foreignBlockchain);
// Send all known trade-bot entries
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
// Optional filtering
if (foreignBlockchain != null)
tradeBotEntries = tradeBotEntries.stream()
.filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain))
.collect(Collectors.toList());
// Maybe send all known trade-bot entries
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> tradeBotEntries = new ArrayList<>();
// We might need to exclude the initial data from the response
if (!excludeInitialData) {
tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
// Optional filtering
if (foreignBlockchain != null)
tradeBotEntries = tradeBotEntries.stream()
.filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain))
.collect(Collectors.toList());
}
if (!sendEntries(session, tradeBotEntries)) {
session.close(4002, "websocket issue");

View File

@@ -173,6 +173,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
final boolean excludeInitialData = queryParams.get("excludeInitialData") != null;
List<String> foreignBlockchains = queryParams.get("foreignBlockchain");
final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
@@ -189,20 +190,23 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
List<CrossChainOfferSummary> crossChainOfferSummaries = new ArrayList<>();
synchronized (cachedInfoByBlockchain) {
Collection<CachedOfferInfo> cachedInfos;
// We might need to exclude the initial data from the response
if (!excludeInitialData) {
synchronized (cachedInfoByBlockchain) {
Collection<CachedOfferInfo> cachedInfos;
if (foreignBlockchain == null)
// No preferred blockchain, so iterate through all of them
cachedInfos = cachedInfoByBlockchain.values();
else
cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
if (foreignBlockchain == null)
// No preferred blockchain, so iterate through all of them
cachedInfos = cachedInfoByBlockchain.values();
else
cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
for (CachedOfferInfo cachedInfo : cachedInfos) {
crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
for (CachedOfferInfo cachedInfo : cachedInfos) {
crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
if (includeHistoric)
crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
if (includeHistoric)
crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
}
}
}

View File

@@ -65,11 +65,15 @@ public class TradePresenceWebSocket extends ApiWebSocket implements Listener {
@Override
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean excludeInitialData = queryParams.get("excludeInitialData") != null;
List<TradePresenceData> tradePresences;
List<TradePresenceData> tradePresences = new ArrayList<>();
synchronized (currentEntries) {
tradePresences = List.copyOf(currentEntries.values());
// We might need to exclude the initial data from the response
if (!excludeInitialData) {
synchronized (currentEntries) {
tradePresences = List.copyOf(currentEntries.values());
}
}
if (!sendTradePresences(session, tradePresences)) {

View File

@@ -93,17 +93,10 @@ public class ArbitraryDataFile {
File outputFile = outputFilePath.toFile();
try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
outputStream.write(fileContent);
outputStream.close();
this.filePath = outputFilePath;
// Verify hash
String digest58 = this.digest58();
if (!this.hash58.equals(digest58)) {
LOGGER.error("Hash {} does not match file digest {} for signature: {}", this.hash58, digest58, Base58.encode(signature));
this.delete();
throw new DataException("Data file digest validation failed");
}
} catch (IOException e) {
throw new DataException("Unable to write data to file");
this.delete();
throw new DataException(String.format("Unable to write data with hash %s: %s", this.hash58, e.getMessage()));
}
}

View File

@@ -170,6 +170,7 @@ public class ArbitraryDataReader {
this.validate();
} catch (DataException e) {
LOGGER.info("DataException when trying to load QDN resource", e);
this.deleteWorkingDirectory();
throw new DataException(e.getMessage());
@@ -208,8 +209,13 @@ public class ArbitraryDataReader {
* serve a cached version of the resource for subsequent requests.
* @throws IOException
*/
private void deleteWorkingDirectory() throws IOException {
FilesystemUtils.safeDeleteDirectory(this.workingPath, true);
private void deleteWorkingDirectory() {
try {
FilesystemUtils.safeDeleteDirectory(this.workingPath, true);
} catch (IOException e) {
// Ignore failures as this isn't an essential step
LOGGER.info("Unable to delete working path {}: {}", this.workingPath, e.getMessage());
}
}
private void createUncompressedDirectory() throws DataException {
@@ -408,6 +414,7 @@ public class ArbitraryDataReader {
this.decryptUsingAlgo("AES/CBC/PKCS5Padding");
} catch (DataException e) {
LOGGER.info("Unable to decrypt using specific parameters: {}", e.getMessage());
// Something went wrong, so fall back to default AES params (necessary for legacy resource support)
this.decryptUsingAlgo("AES");
@@ -420,8 +427,9 @@ public class ArbitraryDataReader {
byte[] secret = this.secret58 != null ? Base58.decode(this.secret58) : null;
if (secret != null && secret.length == Transformer.AES256_LENGTH) {
try {
LOGGER.info("Decrypting using algorithm {}...", algorithm);
Path unencryptedPath = Paths.get(this.workingPath.toString(), "zipped.zip");
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, algorithm);
SecretKey aesKey = new SecretKeySpec(secret, 0, secret.length, "AES");
AES.decryptFile(algorithm, aesKey, this.filePath.toString(), unencryptedPath.toString());
// Replace filePath pointer with the encrypted file path
@@ -430,7 +438,8 @@ public class ArbitraryDataReader {
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException
| BadPaddingException | IllegalBlockSizeException | IOException | InvalidKeyException e) {
throw new DataException(String.format("Unable to decrypt file at path %s: %s", this.filePath, e.getMessage()));
LOGGER.info(String.format("Exception when decrypting using algorithm %s", algorithm), e);
throw new DataException(String.format("Unable to decrypt file at path %s using algorithm %s: %s", this.filePath, algorithm, e.getMessage()));
}
} else {
// Assume it is unencrypted. This will be the case when we have built a custom path by combining
@@ -477,7 +486,12 @@ public class ArbitraryDataReader {
// Delete original compressed file
if (FilesystemUtils.pathInsideDataOrTempPath(this.filePath)) {
if (Files.exists(this.filePath)) {
Files.delete(this.filePath);
try {
Files.delete(this.filePath);
} catch (IOException e) {
// Ignore failures as this isn't an essential step
LOGGER.info("Unable to delete file at path {}", this.filePath);
}
}
}

View File

@@ -1,16 +1,18 @@
package org.qortal.arbitrary.misc;
import org.apache.commons.io.FilenameUtils;
import org.json.JSONObject;
import org.qortal.arbitrary.ArbitraryDataRenderer;
import org.qortal.transaction.Transaction;
import org.qortal.utils.FilesystemUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
@@ -38,6 +40,7 @@ public enum Service {
GIT_REPOSITORY(300, false, null, null),
IMAGE(400, true, 10*1024*1024L, null),
THUMBNAIL(410, true, 500*1024L, null),
QCHAT_IMAGE(420, true, 500*1024L, null),
VIDEO(500, false, null, null),
AUDIO(600, false, null, null),
BLOG(700, false, null, null),
@@ -48,7 +51,30 @@ public enum Service {
PLAYLIST(910, true, null, null),
APP(1000, false, null, null),
METADATA(1100, false, null, null),
QORTAL_METADATA(1111, true, 10*1024L, Arrays.asList("title", "description", "tags"));
GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
@Override
public ValidationResult validate(Path path) {
// Custom validation function to require .gif files only, and at least 1
int gifCount = 0;
File[] files = path.toFile().listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
}
String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
if (!Objects.equals(extension, "gif")) {
return ValidationResult.INVALID_FILE_EXTENSION;
}
gifCount++;
}
}
if (gifCount == 0) {
return ValidationResult.MISSING_DATA;
}
return ValidationResult.OK;
}
};
public final int value;
private final boolean requiresValidation;
@@ -114,7 +140,10 @@ public enum Service {
OK(1),
MISSING_KEYS(2),
EXCEEDS_SIZE_LIMIT(3),
MISSING_INDEX_FILE(4);
MISSING_INDEX_FILE(4),
DIRECTORIES_NOT_ALLOWED(5),
INVALID_FILE_EXTENSION(6),
MISSING_DATA(7);
public final int value;

View File

@@ -3,9 +3,12 @@ package org.qortal.block;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.*;
@@ -24,6 +27,7 @@ import org.qortal.block.BlockChain.BlockTimingByHeight;
import org.qortal.block.BlockChain.AccountLevelShareBin;
import org.qortal.controller.OnlineAccountsManager;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.Qortal25519Extras;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.EligibleQoraHolderData;
@@ -85,7 +89,8 @@ public class Block {
ONLINE_ACCOUNT_UNKNOWN(71),
ONLINE_ACCOUNT_SIGNATURES_MISSING(72),
ONLINE_ACCOUNT_SIGNATURES_MALFORMED(73),
ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74);
ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74),
ONLINE_ACCOUNT_NONCE_INCORRECT(75);
public final int value;
@@ -118,6 +123,8 @@ public class Block {
/** Remote/imported/loaded AT states */
protected List<ATStateData> atStates;
/** Remote hash of AT states - in lieu of full AT state data in {@code atStates} */
protected byte[] atStatesHash;
/** Locally-generated AT states */
protected List<ATStateData> ourAtStates;
/** Locally-generated AT fees */
@@ -178,8 +185,11 @@ public class Block {
if (accountLevel <= 0)
return null; // level 0 isn't included in any share bins
// Select the correct set of share bins based on block height
final BlockChain blockChain = BlockChain.getInstance();
final AccountLevelShareBin[] shareBinsByLevel = blockChain.getShareBinsByAccountLevel();
final AccountLevelShareBin[] shareBinsByLevel = (blockHeight >= blockChain.getSharesByLevelV2Height()) ?
blockChain.getShareBinsByAccountLevelV2() : blockChain.getShareBinsByAccountLevelV1();
if (accountLevel > shareBinsByLevel.length)
return null;
@@ -192,6 +202,11 @@ public class Block {
}
public boolean hasShareBin(AccountLevelShareBin shareBin, int blockHeight) {
AccountLevelShareBin ourShareBin = this.getShareBin(blockHeight);
return ourShareBin != null && shareBin.id == ourShareBin.id;
}
public long distribute(long accountAmount, Map<String, Long> balanceChanges) {
if (this.isRecipientAlsoMinter) {
// minter & recipient the same - simpler case
@@ -216,11 +231,10 @@ public class Block {
return accountAmount;
}
}
/** Always use getExpandedAccounts() to access this, as it's lazy-instantiated. */
private List<ExpandedAccount> cachedExpandedAccounts = null;
/** Opportunistic cache of this block's valid online accounts. Only created by call to isValid(). */
private List<OnlineAccountData> cachedValidOnlineAccounts = null;
/** Opportunistic cache of this block's valid online reward-shares. Only created by call to isValid(). */
private List<RewardShareData> cachedOnlineRewardShares = null;
@@ -255,7 +269,7 @@ public class Block {
* Constructs new Block using passed transaction and AT states.
* <p>
* This constructor typically used when receiving a serialized block over the network.
*
*
* @param repository
* @param blockData
* @param transactions
@@ -281,6 +295,35 @@ public class Block {
this.blockData.setTotalFees(totalFees);
}
/**
* Constructs new Block using passed transaction and minimal AT state info.
* <p>
* This constructor typically used when receiving a serialized block over the network.
*
* @param repository
* @param blockData
* @param transactions
* @param atStatesHash
*/
public Block(Repository repository, BlockData blockData, List<TransactionData> transactions, byte[] atStatesHash) {
this(repository, blockData);
this.transactions = new ArrayList<>();
long totalFees = 0;
// We have to sum fees too
for (TransactionData transactionData : transactions) {
this.transactions.add(Transaction.fromData(repository, transactionData));
totalFees += transactionData.getFee();
}
this.atStatesHash = atStatesHash;
totalFees += this.blockData.getATFees();
this.blockData.setTotalFees(totalFees);
}
/**
* Constructs new Block with empty transaction list, using passed minter account.
*
@@ -313,18 +356,22 @@ public class Block {
int version = parentBlock.getNextBlockVersion();
byte[] reference = parentBlockData.getSignature();
// Fetch our list of online accounts
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
if (onlineAccounts.isEmpty()) {
LOGGER.error("No online accounts - not even our own?");
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
if (minterLevel == 0) {
LOGGER.error("Minter effective level returned zero?");
return null;
}
// Find newest online accounts timestamp
long onlineAccountsTimestamp = 0;
for (OnlineAccountData onlineAccountData : onlineAccounts) {
if (onlineAccountData.getTimestamp() > onlineAccountsTimestamp)
onlineAccountsTimestamp = onlineAccountData.getTimestamp();
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
// Fetch our list of online accounts, removing any that are missing a nonce
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
if (onlineAccounts.isEmpty()) {
LOGGER.debug("No online accounts - not even our own?");
return null;
}
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
@@ -335,10 +382,6 @@ public class Block {
// Map using index into sorted list of reward-shares as key
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
for (OnlineAccountData onlineAccountData : onlineAccounts) {
// Disregard online accounts with different timestamps
if (onlineAccountData.getTimestamp() != onlineAccountsTimestamp)
continue;
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
if (accountIndex == null)
// Online account (reward-share) with current timestamp but reward-share cancelled
@@ -355,26 +398,41 @@ public class Block {
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
int onlineAccountsCount = onlineAccountsSet.size();
// Concatenate online account timestamp signatures (in correct order)
byte[] onlineAccountsSignatures = new byte[onlineAccountsCount * Transformer.SIGNATURE_LENGTH];
for (int i = 0; i < onlineAccountsCount; ++i) {
Integer accountIndex = accountIndexes.get(i);
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
System.arraycopy(onlineAccountData.getSignature(), 0, onlineAccountsSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
// Collate all signatures
Collection<byte[]> signaturesToAggregate = indexedOnlineAccounts.values()
.stream()
.map(OnlineAccountData::getSignature)
.collect(Collectors.toList());
// Aggregated, single signature
byte[] onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
// Add nonces to the end of the online accounts signatures
try {
// Create ordered list of nonce values
List<Integer> nonces = new ArrayList<>();
for (int i = 0; i < onlineAccountsCount; ++i) {
Integer accountIndex = accountIndexes.get(i);
OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
nonces.add(onlineAccountData.getNonce());
}
// Encode the nonces to a byte array
byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
// Append the encoded nonces to the encoded online account signatures
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(onlineAccountsSignatures);
outputStream.write(encodedNonces);
onlineAccountsSignatures = outputStream.toByteArray();
}
catch (TransformationException | IOException e) {
return null;
}
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
minter.getPublicKey(), encodedOnlineAccounts));
// Qortal: minter is always a reward-share, so find actual minter and get their effective minting level
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, minter.getPublicKey());
if (minterLevel == 0) {
LOGGER.error("Minter effective level returned zero?");
return null;
}
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
int transactionCount = 0;
byte[] transactionsSignature = null;
int height = parentBlockData.getHeight() + 1;
@@ -979,49 +1037,64 @@ public class Block {
if (this.blockData.getOnlineAccountsSignatures() == null || this.blockData.getOnlineAccountsSignatures().length == 0)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
if (this.blockData.getOnlineAccountsSignatures().length != onlineRewardShares.size() * Transformer.SIGNATURE_LENGTH)
final int signaturesLength = Transformer.SIGNATURE_LENGTH;
final int noncesLength = onlineRewardShares.size() * Transformer.INT_LENGTH;
// We expect nonces to be appended to the online accounts signatures
if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength + noncesLength)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
// Check signatures
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
byte[] onlineTimestampBytes = Longs.toByteArray(onlineTimestamp);
// If this block is much older than current online timestamp, then there's no point checking current online accounts
List<OnlineAccountData> currentOnlineAccounts = onlineTimestamp < NTP.getTime() - OnlineAccountsManager.ONLINE_TIMESTAMP_MODULUS
? null
: OnlineAccountsManager.getInstance().getOnlineAccounts();
List<OnlineAccountData> latestBlocksOnlineAccounts = OnlineAccountsManager.getInstance().getLatestBlocksOnlineAccounts();
byte[] encodedOnlineAccountSignatures = this.blockData.getOnlineAccountsSignatures();
// Extract online accounts' timestamp signatures from block data
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
// Split online account signatures into signature(s) + nonces, then validate the nonces
byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength);
byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH);
encodedOnlineAccountSignatures = extractedSignatures;
// We'll build up a list of online accounts to hand over to Controller if block is added to chain
// and this will become latestBlocksOnlineAccounts (above) to reduce CPU load when we process next block...
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
List<Integer> nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
for (int i = 0; i < onlineAccountsSignatures.size(); ++i) {
byte[] signature = onlineAccountsSignatures.get(i);
// Build block's view of online accounts (without signatures, as we don't need them here)
Set<OnlineAccountData> onlineAccounts = new HashSet<>();
for (int i = 0; i < onlineRewardShares.size(); ++i) {
Integer nonce = nonces.get(i);
byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, signature, publicKey);
ourOnlineAccounts.add(onlineAccountData);
// If signature is still current then no need to perform Ed25519 verify
if (currentOnlineAccounts != null && currentOnlineAccounts.remove(onlineAccountData))
// remove() returned true, so online account still current
// and one less entry in currentOnlineAccounts to check next time
continue;
// If signature was okay in latest block then no need to perform Ed25519 verify
if (latestBlocksOnlineAccounts != null && latestBlocksOnlineAccounts.contains(onlineAccountData))
continue;
if (!Crypto.verify(publicKey, signature, onlineTimestampBytes))
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, null, publicKey, nonce);
onlineAccounts.add(onlineAccountData);
}
// Remove those already validated & cached by online accounts manager - no need to re-validate them
OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
// Validate the rest
for (OnlineAccountData onlineAccount : onlineAccounts)
if (!OnlineAccountsManager.getInstance().verifyMemoryPoW(onlineAccount, null))
return ValidationResult.ONLINE_ACCOUNT_NONCE_INCORRECT;
// Cache the valid online accounts as they will likely be needed for the next block
OnlineAccountsManager.getInstance().addBlocksOnlineAccounts(onlineAccounts, onlineTimestamp);
// Extract online accounts' timestamp signatures from block data. Only one signature if aggregated.
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(encodedOnlineAccountSignatures);
// Aggregate all public keys
Collection<byte[]> publicKeys = onlineRewardShares.stream()
.map(RewardShareData::getRewardSharePublicKey)
.collect(Collectors.toList());
byte[] aggregatePublicKey = Qortal25519Extras.aggregatePublicKeys(publicKeys);
byte[] aggregateSignature = onlineAccountsSignatures.get(0);
// One-step verification of aggregate signature using aggregate public key
if (!Qortal25519Extras.verifyAggregated(aggregatePublicKey, aggregateSignature, onlineTimestampBytes))
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
// All online accounts valid, so save our list of online accounts for potential later use
this.cachedValidOnlineAccounts = ourOnlineAccounts;
this.cachedOnlineRewardShares = onlineRewardShares;
return ValidationResult.OK;
@@ -1168,6 +1241,7 @@ public class Block {
}
}
} catch (DataException e) {
LOGGER.info("DataException during transaction validation", e);
return ValidationResult.TRANSACTION_INVALID;
} finally {
// Rollback repository changes made by test-processing transactions above
@@ -1194,7 +1268,7 @@ public class Block {
*/
private ValidationResult areAtsValid() throws DataException {
// Locally generated AT states should be valid so no need to re-execute them
if (this.ourAtStates == this.getATStates()) // Note object reference compare
if (this.ourAtStates != null && this.ourAtStates == this.atStates) // Note object reference compare
return ValidationResult.OK;
// Generate local AT states for comparison
@@ -1208,8 +1282,33 @@ public class Block {
if (this.ourAtFees != this.blockData.getATFees())
return ValidationResult.AT_STATES_MISMATCH;
// Note: this.atStates fully loaded thanks to this.getATStates() call above
for (int s = 0; s < this.atStates.size(); ++s) {
// If we have a single AT states hash then compare that in preference
if (this.atStatesHash != null) {
int atBytesLength = blockData.getATCount() * BlockTransformer.AT_ENTRY_LENGTH;
ByteArrayOutputStream atHashBytes = new ByteArrayOutputStream(atBytesLength);
try {
for (ATStateData atStateData : this.ourAtStates) {
atHashBytes.write(atStateData.getATAddress().getBytes(StandardCharsets.UTF_8));
atHashBytes.write(atStateData.getStateHash());
atHashBytes.write(Longs.toByteArray(atStateData.getFees()));
}
} catch (IOException e) {
throw new DataException("Couldn't validate AT states hash due to serialization issue?", e);
}
byte[] ourAtStatesHash = Crypto.digest(atHashBytes.toByteArray());
if (!Arrays.equals(ourAtStatesHash, this.atStatesHash))
return ValidationResult.AT_STATES_MISMATCH;
// Use our AT state data from now on
this.atStates = this.ourAtStates;
return ValidationResult.OK;
}
// Note: this.atStates fully loaded thanks to this.getATStates() call:
this.getATStates();
for (int s = 0; s < this.ourAtStates.size(); ++s) {
ATStateData ourAtState = this.ourAtStates.get(s);
ATStateData theirAtState = this.atStates.get(s);
@@ -1367,9 +1466,6 @@ public class Block {
postBlockTidy();
// Give Controller our cached, valid online accounts data (if any) to help reduce CPU load for next block
OnlineAccountsManager.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
// Log some debugging info relating to the block weight calculation
this.logDebugInfo();
}
@@ -1585,9 +1681,6 @@ public class Block {
this.blockData.setHeight(null);
postBlockTidy();
// Remove any cached, valid online accounts data from Controller
OnlineAccountsManager.getInstance().popLatestBlocksOnlineAccounts();
}
protected void orphanTransactionsFromBlock() throws DataException {
@@ -1824,13 +1917,72 @@ public class Block {
final List<ExpandedAccount> onlineFounderAccounts = expandedAccounts.stream().filter(expandedAccount -> expandedAccount.isMinterFounder).collect(Collectors.toList());
final boolean haveFounders = !onlineFounderAccounts.isEmpty();
// Select the correct set of share bins based on block height
List<AccountLevelShareBin> accountLevelShareBinsForBlock = (this.blockData.getHeight() >= BlockChain.getInstance().getSharesByLevelV2Height()) ?
BlockChain.getInstance().getAccountLevelShareBinsV2() : BlockChain.getInstance().getAccountLevelShareBinsV1();
// Determine reward candidates based on account level
List<AccountLevelShareBin> accountLevelShareBins = BlockChain.getInstance().getAccountLevelShareBins();
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
// This needs a deep copy, so the shares can be modified when tiers aren't activated yet
List<AccountLevelShareBin> accountLevelShareBins = new ArrayList<>();
for (AccountLevelShareBin accountLevelShareBin : accountLevelShareBinsForBlock) {
accountLevelShareBins.add((AccountLevelShareBin) accountLevelShareBin.clone());
}
Map<Integer, List<ExpandedAccount>> accountsForShareBin = new HashMap<>();
// We might need to combine some share bins if they haven't reached the minimum number of minters yet
for (int binIndex = accountLevelShareBins.size()-1; binIndex >= 0; --binIndex) {
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
// Object reference compare is OK as all references are read-only from blockchain config.
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.getShareBin(this.blockData.getHeight()) == accountLevelShareBin).collect(Collectors.toList());
// Find all accounts in share bin. getShareBin() returns null for minter accounts that are also founders, so they are effectively filtered out.
List<ExpandedAccount> binnedAccounts = expandedAccounts.stream().filter(accountInfo -> accountInfo.hasShareBin(accountLevelShareBin, this.blockData.getHeight())).collect(Collectors.toList());
// Add any accounts that have been moved down from a higher tier
List<ExpandedAccount> existingBinnedAccounts = accountsForShareBin.get(binIndex);
if (existingBinnedAccounts != null)
binnedAccounts.addAll(existingBinnedAccounts);
// Logic below may only apply to higher levels, and only for share bins with a specific range of online accounts
if (accountLevelShareBin.levels.get(0) < BlockChain.getInstance().getShareBinActivationMinLevel() ||
binnedAccounts.isEmpty() || binnedAccounts.size() >= BlockChain.getInstance().getMinAccountsToActivateShareBin()) {
// Add all accounts for this share bin to the accountsForShareBin list
accountsForShareBin.put(binIndex, binnedAccounts);
continue;
}
// Share bin contains more than one, but less than the minimum number of minters. We treat this share bin
// as not activated yet. In these cases, the rewards and minters are combined and paid out to the previous
// share bin, to prevent a single or handful of accounts receiving the entire rewards for a share bin.
//
// Example:
//
// - Share bin for levels 5 and 6 has 100 minters
// - Share bin for levels 7 and 8 has 10 minters
//
// This is below the minimum of 30, so share bins are reconstructed as follows:
//
// - Share bin for levels 5 and 6 now contains 110 minters
// - Share bin for levels 7 and 8 now contains 0 minters
// - Share bin for levels 5 and 6 now pays out rewards for levels 5, 6, 7, and 8
// - Share bin for levels 7 and 8 pays zero rewards
//
// This process is iterative, so will combine several tiers if needed.
// Designate this share bin as empty
accountsForShareBin.put(binIndex, new ArrayList<>());
// Move the accounts originally intended for this share bin to the previous one
accountsForShareBin.put(binIndex - 1, binnedAccounts);
// Move the block reward from this share bin to the previous one
AccountLevelShareBin previousShareBin = accountLevelShareBins.get(binIndex - 1);
previousShareBin.share += accountLevelShareBin.share;
accountLevelShareBin.share = 0L;
}
// Now loop through (potentially modified) share bins and determine the reward candidates
for (int binIndex = 0; binIndex < accountLevelShareBins.size(); ++binIndex) {
AccountLevelShareBin accountLevelShareBin = accountLevelShareBins.get(binIndex);
List<ExpandedAccount> binnedAccounts = accountsForShareBin.get(binIndex);
// No online accounts in this bin? Skip to next one
if (binnedAccounts.isEmpty())
@@ -1848,7 +2000,7 @@ public class Block {
// Fetch list of legacy QORA holders who haven't reached their cap of QORT reward.
List<EligibleQoraHolderData> qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight());
final boolean haveQoraHolders = !qoraHolders.isEmpty();
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare();
final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight());
// Perform account-level-based reward scaling if appropriate
if (!haveFounders) {

View File

@@ -68,8 +68,14 @@ public class BlockChain {
atFindNextTransactionFix,
newBlockSigHeight,
shareBinFix,
sharesByLevelV2Height,
rewardShareLimitTimestamp,
calcChainWeightTimestamp,
transactionV5Timestamp;
transactionV5Timestamp,
transactionV6Timestamp,
disableReferenceTimestamp,
increaseOnlineAccountsDifficultyTimestamp,
chatReferenceTimestamp;
}
// Custom transaction fees
@@ -100,23 +106,48 @@ public class BlockChain {
private List<RewardByHeight> rewardsByHeight;
/** Share of block reward/fees by account level */
public static class AccountLevelShareBin {
public static class AccountLevelShareBin implements Cloneable {
public int id;
public List<Integer> levels;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long share;
}
private List<AccountLevelShareBin> sharesByLevel;
/** Generated lookup of share-bin by account level */
private AccountLevelShareBin[] shareBinsByLevel;
/** Share of block reward/fees to legacy QORA coin holders */
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long qoraHoldersShare;
public Object clone() {
AccountLevelShareBin shareBinCopy = new AccountLevelShareBin();
List<Integer> levelsCopy = new ArrayList<>();
for (Integer level : this.levels) {
levelsCopy.add(level);
}
shareBinCopy.id = this.id;
shareBinCopy.levels = levelsCopy;
shareBinCopy.share = this.share;
return shareBinCopy;
}
}
private List<AccountLevelShareBin> sharesByLevelV1;
private List<AccountLevelShareBin> sharesByLevelV2;
/** Generated lookup of share-bin by account level */
private AccountLevelShareBin[] shareBinsByLevelV1;
private AccountLevelShareBin[] shareBinsByLevelV2;
/** Share of block reward/fees to legacy QORA coin holders, by block height */
public static class ShareByHeight {
public int height;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long share;
}
private List<ShareByHeight> qoraHoldersShareByHeight;
/** How many legacy QORA per 1 QORT of block reward. */
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long qoraPerQortReward;
/** Minimum number of accounts before a share bin is considered activated */
private int minAccountsToActivateShareBin;
/** Min level at which share bin activation takes place; lower levels allow less than minAccountsPerShareBin */
private int shareBinActivationMinLevel;
/**
* Number of minted blocks required to reach next level from previous.
* <p>
@@ -154,7 +185,7 @@ public class BlockChain {
private int minAccountLevelToMint;
private int minAccountLevelForBlockSubmissions;
private int minAccountLevelToRewardShare;
private int maxRewardSharesPerMintingAccount;
private int maxRewardSharesPerFounderMintingAccount;
private int founderEffectiveMintingLevel;
/** Minimum time to retain online account signatures (ms) for block validity checks. */
@@ -162,6 +193,17 @@ public class BlockChain {
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
private long onlineAccountSignaturesMaxLifetime;
/** Feature trigger timestamp for ONLINE_ACCOUNTS_MODULUS time interval increase. Can't use
* featureTriggers because unit tests need to set this value via Reflection. */
private long onlineAccountsModulusV2Timestamp;
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
public int maxShares;
}
private List<MaxRewardSharesByTimestamp> maxRewardSharesByTimestamp;
/** Settings relating to CIYAM AT feature. */
public static class CiyamAtSettings {
/** Fee per step/op-code executed. */
@@ -310,6 +352,11 @@ public class BlockChain {
return this.maxBlockSize;
}
// Online accounts
public long getOnlineAccountsModulusV2Timestamp() {
return this.onlineAccountsModulusV2Timestamp;
}
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
public boolean getRequireGroupForApproval() {
return this.requireGroupForApproval;
@@ -327,12 +374,20 @@ public class BlockChain {
return this.rewardsByHeight;
}
public List<AccountLevelShareBin> getAccountLevelShareBins() {
return this.sharesByLevel;
public List<AccountLevelShareBin> getAccountLevelShareBinsV1() {
return this.sharesByLevelV1;
}
public AccountLevelShareBin[] getShareBinsByAccountLevel() {
return this.shareBinsByLevel;
public List<AccountLevelShareBin> getAccountLevelShareBinsV2() {
return this.sharesByLevelV2;
}
public AccountLevelShareBin[] getShareBinsByAccountLevelV1() {
return this.shareBinsByLevelV1;
}
public AccountLevelShareBin[] getShareBinsByAccountLevelV2() {
return this.shareBinsByLevelV2;
}
public List<Integer> getBlocksNeededByLevel() {
@@ -343,14 +398,18 @@ public class BlockChain {
return this.cumulativeBlocksByLevel;
}
public long getQoraHoldersShare() {
return this.qoraHoldersShare;
}
public long getQoraPerQortReward() {
return this.qoraPerQortReward;
}
public int getMinAccountsToActivateShareBin() {
return this.minAccountsToActivateShareBin;
}
public int getShareBinActivationMinLevel() {
return this.shareBinActivationMinLevel;
}
public int getMinAccountLevelToMint() {
return this.minAccountLevelToMint;
}
@@ -363,8 +422,8 @@ public class BlockChain {
return this.minAccountLevelToRewardShare;
}
public int getMaxRewardSharesPerMintingAccount() {
return this.maxRewardSharesPerMintingAccount;
public int getMaxRewardSharesPerFounderMintingAccount() {
return this.maxRewardSharesPerFounderMintingAccount;
}
public int getFounderEffectiveMintingLevel() {
@@ -397,6 +456,14 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.shareBinFix.name()).intValue();
}
public int getSharesByLevelV2Height() {
return this.featureTriggers.get(FeatureTrigger.sharesByLevelV2Height.name()).intValue();
}
public long getRewardShareLimitTimestamp() {
return this.featureTriggers.get(FeatureTrigger.rewardShareLimitTimestamp.name()).longValue();
}
public long getCalcChainWeightTimestamp() {
return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue();
}
@@ -405,6 +472,23 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.transactionV5Timestamp.name()).longValue();
}
public long getTransactionV6Timestamp() {
return this.featureTriggers.get(FeatureTrigger.transactionV6Timestamp.name()).longValue();
}
public long getDisableReferenceTimestamp() {
return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue();
}
public long getIncreaseOnlineAccountsDifficultyTimestamp() {
return this.featureTriggers.get(FeatureTrigger.increaseOnlineAccountsDifficultyTimestamp.name()).longValue();
}
public long getChatReferenceTimestamp() {
return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue();
}
// More complex getters for aspects that change by height or timestamp
public long getRewardAtHeight(int ourHeight) {
@@ -433,6 +517,23 @@ public class BlockChain {
return this.getUnitFee();
}
public int getMaxRewardSharesAtTimestamp(long ourTimestamp) {
for (int i = maxRewardSharesByTimestamp.size() - 1; i >= 0; --i)
if (maxRewardSharesByTimestamp.get(i).timestamp <= ourTimestamp)
return maxRewardSharesByTimestamp.get(i).maxShares;
return 0;
}
public long getQoraHoldersShareAtHeight(int ourHeight) {
// Scan through for QORA share at our height
for (int i = qoraHoldersShareByHeight.size() - 1; i >= 0; --i)
if (qoraHoldersShareByHeight.get(i).height <= ourHeight)
return qoraHoldersShareByHeight.get(i).share;
return 0;
}
/** Validate blockchain config read from JSON */
private void validateConfig() {
if (this.genesisInfo == null)
@@ -441,11 +542,14 @@ public class BlockChain {
if (this.rewardsByHeight == null)
Settings.throwValidationError("No \"rewardsByHeight\" entry found in blockchain config");
if (this.sharesByLevel == null)
Settings.throwValidationError("No \"sharesByLevel\" entry found in blockchain config");
if (this.sharesByLevelV1 == null)
Settings.throwValidationError("No \"sharesByLevelV1\" entry found in blockchain config");
if (this.qoraHoldersShare == null)
Settings.throwValidationError("No \"qoraHoldersShare\" entry found in blockchain config");
if (this.sharesByLevelV2 == null)
Settings.throwValidationError("No \"sharesByLevelV2\" entry found in blockchain config");
if (this.qoraHoldersShareByHeight == null)
Settings.throwValidationError("No \"qoraHoldersShareByHeight\" entry found in blockchain config");
if (this.qoraPerQortReward == null)
Settings.throwValidationError("No \"qoraPerQortReward\" entry found in blockchain config");
@@ -482,13 +586,22 @@ public class BlockChain {
if (!this.featureTriggers.containsKey(featureTrigger.name()))
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
// Check block reward share bounds
long totalShare = this.qoraHoldersShare;
// Check block reward share bounds (V1)
long totalShareV1 = this.qoraHoldersShareByHeight.get(0).share;
// Add share percents for account-level-based rewards
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
totalShare += accountLevelShareBin.share;
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV1)
totalShareV1 += accountLevelShareBin.share;
if (totalShare < 0 || totalShare > 1_00000000L)
if (totalShareV1 < 0 || totalShareV1 > 1_00000000L)
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
// Check block reward share bounds (V2)
long totalShareV2 = this.qoraHoldersShareByHeight.get(1).share;
// Add share percents for account-level-based rewards
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV2)
totalShareV2 += accountLevelShareBin.share;
if (totalShareV2 < 0 || totalShareV2 > 1_00000000L)
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
}
@@ -504,23 +617,34 @@ public class BlockChain {
cumulativeBlocks += this.blocksNeededByLevel.get(level);
}
// Generate lookup-array for account-level share bins
AccountLevelShareBin lastAccountLevelShareBin = this.sharesByLevel.get(this.sharesByLevel.size() - 1);
final int lastLevel = lastAccountLevelShareBin.levels.get(lastAccountLevelShareBin.levels.size() - 1);
this.shareBinsByLevel = new AccountLevelShareBin[lastLevel];
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
// Generate lookup-array for account-level share bins (V1)
AccountLevelShareBin lastAccountLevelShareBinV1 = this.sharesByLevelV1.get(this.sharesByLevelV1.size() - 1);
final int lastLevelV1 = lastAccountLevelShareBinV1.levels.get(lastAccountLevelShareBinV1.levels.size() - 1);
this.shareBinsByLevelV1 = new AccountLevelShareBin[lastLevelV1];
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV1)
for (int level : accountLevelShareBin.levels)
// level 1 stored at index 0, level 2 stored at index 1, etc.
// level 0 not allowed
this.shareBinsByLevel[level - 1] = accountLevelShareBin;
this.shareBinsByLevelV1[level - 1] = accountLevelShareBin;
// Generate lookup-array for account-level share bins (V2)
AccountLevelShareBin lastAccountLevelShareBinV2 = this.sharesByLevelV2.get(this.sharesByLevelV2.size() - 1);
final int lastLevelV2 = lastAccountLevelShareBinV2.levels.get(lastAccountLevelShareBinV2.levels.size() - 1);
this.shareBinsByLevelV2 = new AccountLevelShareBin[lastLevelV2];
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV2)
for (int level : accountLevelShareBin.levels)
// level 1 stored at index 0, level 2 stored at index 1, etc.
// level 0 not allowed
this.shareBinsByLevelV2[level - 1] = accountLevelShareBin;
// Convert collections to unmodifiable form
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel);
this.sharesByLevelV1 = Collections.unmodifiableList(this.sharesByLevelV1);
this.sharesByLevelV2 = Collections.unmodifiableList(this.sharesByLevelV2);
this.blocksNeededByLevel = Collections.unmodifiableList(this.blocksNeededByLevel);
this.cumulativeBlocksByLevel = Collections.unmodifiableList(this.cumulativeBlocksByLevel);
this.blockTimingsByHeight = Collections.unmodifiableList(this.blockTimingsByHeight);
this.qoraHoldersShareByHeight = Collections.unmodifiableList(this.qoraHoldersShareByHeight);
}
/**

View File

@@ -15,6 +15,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -40,6 +41,7 @@ public class AutoUpdate extends Thread {
public static final String JAR_FILENAME = "qortal.jar";
public static final String NEW_JAR_FILENAME = "new-" + JAR_FILENAME;
public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib=";
private static final Logger LOGGER = LogManager.getLogger(AutoUpdate.class);
private static final long CHECK_INTERVAL = 20 * 60 * 1000L; // ms
@@ -243,6 +245,11 @@ public class AutoUpdate extends Thread {
// JVM arguments
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
// Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
javaCmd = javaCmd.stream()
.map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
.collect(Collectors.toList());
// Remove JNI options as they won't be supported by command-line 'java'
// These are typically added by the AdvancedInstaller Java launcher EXE
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
@@ -261,10 +268,19 @@ public class AutoUpdate extends Thread {
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"),
MessageType.INFO);
new ProcessBuilder(javaCmd).start();
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
// New process will inherit our stdout and stderr
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
// Nothing to pipe to new process, so close output stream (process's stdin)
process.getOutputStream().close();
return true; // applying update OK
} catch (IOException e) {
} catch (Exception e) {
LOGGER.error(String.format("Failed to apply update: %s", e.getMessage()));
try {

View File

@@ -26,6 +26,9 @@ import org.qortal.data.block.CommonBlockData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.message.BlockSummariesV2Message;
import org.qortal.network.message.HeightV2Message;
import org.qortal.network.message.Message;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -61,8 +64,12 @@ public class BlockMinter extends Thread {
public void run() {
Thread.currentThread().setName("BlockMinter");
try (final Repository repository = RepositoryManager.getRepository()) {
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
if (Settings.getInstance().isLite()) {
// Lite nodes do not mint
return;
}
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
try (final Repository repository = RepositoryManager.getRepository()) {
// Wipe existing unconfirmed transactions
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
@@ -72,356 +79,377 @@ public class BlockMinter extends Thread {
}
repository.saveChanges();
} catch (DataException e) {
LOGGER.warn("Repository issue trying to wipe unconfirmed transactions on start-up: {}", e.getMessage());
// Fall-through to normal behaviour in case we can recover
}
}
BlockData previousBlockData = null;
// Vars to keep track of blocks that were skipped due to chain weight
byte[] parentSignatureForLastLowWeightBlock = null;
Long timeOfLastLowWeightBlock = null;
List<Block> newBlocks = new ArrayList<>();
final boolean isSingleNodeTestnet = Settings.getInstance().isSingleNodeTestnet();
try (final Repository repository = RepositoryManager.getRepository()) {
// Going to need this a lot...
BlockRepository blockRepository = repository.getBlockRepository();
BlockData previousBlockData = null;
// Vars to keep track of blocks that were skipped due to chain weight
byte[] parentSignatureForLastLowWeightBlock = null;
Long timeOfLastLowWeightBlock = null;
List<Block> newBlocks = new ArrayList<>();
// Flags for tracking change in whether minting is possible,
// so we can notify Controller, and further update SysTray, etc.
boolean isMintingPossible = false;
boolean wasMintingPossible = isMintingPossible;
while (running) {
repository.discardChanges(); // Free repository locks, if any
if (isMintingPossible != wasMintingPossible)
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
wasMintingPossible = isMintingPossible;
// Sleep for a while
Thread.sleep(1000);
isMintingPossible = false;
final Long now = NTP.getTime();
if (now == null)
continue;
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
if (minLatestBlockTimestamp == null)
continue;
// No online accounts? (e.g. during startup)
if (OnlineAccountsManager.getInstance().getOnlineAccounts().isEmpty())
continue;
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
// No minting accounts?
if (mintingAccountsData.isEmpty())
continue;
// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
// Note that minting accounts are actually reward-shares in Qortal
Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
while (madi.hasNext()) {
MintingAccountData mintingAccountData = madi.next();
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
if (rewardShareData == null) {
// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
madi.remove();
continue;
}
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
if (!mintingAccount.canMint()) {
// Minting-account component of reward-share can no longer mint - disregard
madi.remove();
continue;
}
// Optional (non-validated) prevention of block submissions below a defined level.
// This is an unvalidated version of Blockchain.minAccountLevelToMint
// and exists only to reduce block candidates by default.
int level = mintingAccount.getEffectiveMintingLevel();
if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
madi.remove();
continue;
}
}
// Needs a mutable copy of the unmodifiableList
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
BlockData lastBlockData = blockRepository.getLastBlock();
// Disregard peers that have "misbehaved" recently
peers.removeIf(Controller.hasMisbehaved);
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
if (Synchronizer.getInstance().getRecoveryMode() == false)
peers.removeIf(Controller.hasNoRecentBlock);
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
continue;
// If we are stuck on an invalid block, we should allow an alternative to be minted
boolean recoverInvalidBlock = false;
if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
// We've had at least one invalid block
long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
// Last valid block was more than 10 mins ago, but we've had an invalid block since then
// Assume that the chain has stalled because there is no alternative valid candidate
// Enter recovery mode to allow alternative, valid candidates to be minted
recoverInvalidBlock = true;
}
}
}
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
continue;
// There are enough peers with a recent block and our latest block is recent
// so go ahead and mint a block if possible.
isMintingPossible = true;
// Check blockchain hasn't changed
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
previousBlockData = lastBlockData;
newBlocks.clear();
// Reduce log timeout
logTimeout = 10 * 1000L;
// Last low weight block is no longer valid
parentSignatureForLastLowWeightBlock = null;
}
// Discard accounts we have already built blocks with
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
// Do we need to build any potential new blocks?
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
// We might need to sit the next block out, if one of our minting accounts signed the previous one
final byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
final boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
if (mintedLastBlock) {
LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
continue;
}
if (parentSignatureForLastLowWeightBlock != null) {
// The last iteration found a higher weight block in the network, so sleep for a while
// to allow is to sync the higher weight chain. We are sleeping here rather than when
// detected as we don't want to hold the blockchain lock open.
LOGGER.debug("Sleeping for 10 seconds...");
Thread.sleep(10 * 1000L);
}
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
// First block does the AT heavy-lifting
if (newBlocks.isEmpty()) {
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
if (newBlock == null) {
// For some reason we can't mint right now
moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
continue;
}
newBlocks.add(newBlock);
} else {
// The blocks for other minters require less effort...
Block newBlock = newBlocks.get(0).remint(mintingAccount);
if (newBlock == null) {
// For some reason we can't mint right now
moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
continue;
}
newBlocks.add(newBlock);
}
}
// No potential block candidates?
if (newBlocks.isEmpty())
continue;
// Make sure we're the only thread modifying the blockchain
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
continue;
}
boolean newBlockMinted = false;
Block newBlock = null;
try {
// Clear repository session state so we have latest view of data
// Free up any repository locks
repository.discardChanges();
// Now that we have blockchain lock, do final check that chain hasn't changed
BlockData latestBlockData = blockRepository.getLastBlock();
if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
// Sleep for a while.
// It's faster on single node testnets, to allow lots of blocks to be minted quickly.
Thread.sleep(isSingleNodeTestnet ? 50 : 1000);
isMintingPossible = false;
final Long now = NTP.getTime();
if (now == null)
continue;
List<Block> goodBlocks = new ArrayList<>();
for (Block testBlock : newBlocks) {
// Is new block's timestamp valid yet?
// We do a separate check as some timestamp checks are skipped for testchains
if (testBlock.isTimestampValid() != ValidationResult.OK)
continue;
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
if (minLatestBlockTimestamp == null)
continue;
testBlock.preProcess();
// No online accounts for current timestamp? (e.g. during startup)
if (!OnlineAccountsManager.getInstance().hasOnlineAccounts())
continue;
// Is new block valid yet? (Before adding unconfirmed transactions)
ValidationResult result = testBlock.isValid();
if (result != ValidationResult.OK) {
moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
// No minting accounts?
if (mintingAccountsData.isEmpty())
continue;
// Disregard minting accounts that are no longer valid, e.g. by transfer/loss of founder flag or account level
// Note that minting accounts are actually reward-shares in Qortal
Iterator<MintingAccountData> madi = mintingAccountsData.iterator();
while (madi.hasNext()) {
MintingAccountData mintingAccountData = madi.next();
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
if (rewardShareData == null) {
// Reward-share doesn't exist - probably cancelled but not yet removed from node's list of minting accounts
madi.remove();
continue;
}
goodBlocks.add(testBlock);
}
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
if (!mintingAccount.canMint()) {
// Minting-account component of reward-share can no longer mint - disregard
madi.remove();
continue;
}
if (goodBlocks.isEmpty())
continue;
// Pick best block
final int parentHeight = previousBlockData.getHeight();
final byte[] parentBlockSignature = previousBlockData.getSignature();
BigInteger bestWeight = null;
for (int bi = 0; bi < goodBlocks.size(); ++bi) {
BlockData blockData = goodBlocks.get(bi).getBlockData();
BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
blockSummaryData.setMinterLevel(minterLevel);
BigInteger blockWeight = Block.calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
if (bestWeight == null || blockWeight.compareTo(bestWeight) < 0) {
newBlock = goodBlocks.get(bi);
bestWeight = blockWeight;
// Optional (non-validated) prevention of block submissions below a defined level.
// This is an unvalidated version of Blockchain.minAccountLevelToMint
// and exists only to reduce block candidates by default.
int level = mintingAccount.getEffectiveMintingLevel();
if (level < BlockChain.getInstance().getMinAccountLevelForBlockSubmissions()) {
madi.remove();
continue;
}
}
try {
if (this.higherWeightChainExists(repository, bestWeight)) {
// Needs a mutable copy of the unmodifiableList
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
BlockData lastBlockData = blockRepository.getLastBlock();
// Check if the base block has updated since the last time we were here
if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
!Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
// We've switched to a different chain, so reset the timer
timeOfLastLowWeightBlock = NTP.getTime();
// Disregard peers that have "misbehaved" recently
peers.removeIf(Controller.hasMisbehaved);
// Disregard peers that don't have a recent block, but only if we're not in recovery mode.
// In that mode, we want to allow minting on top of older blocks, to recover stalled networks.
if (Synchronizer.getInstance().getRecoveryMode() == false)
peers.removeIf(Controller.hasNoRecentBlock);
// Don't mint if we don't have enough up-to-date peers as where would the transactions/consensus come from?
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
continue;
// If we are stuck on an invalid block, we should allow an alternative to be minted
boolean recoverInvalidBlock = false;
if (Synchronizer.getInstance().timeInvalidBlockLastReceived != null) {
// We've had at least one invalid block
long timeSinceLastValidBlock = NTP.getTime() - Synchronizer.getInstance().timeValidBlockLastReceived;
long timeSinceLastInvalidBlock = NTP.getTime() - Synchronizer.getInstance().timeInvalidBlockLastReceived;
if (timeSinceLastValidBlock > INVALID_BLOCK_RECOVERY_TIMEOUT) {
if (timeSinceLastInvalidBlock < INVALID_BLOCK_RECOVERY_TIMEOUT) {
// Last valid block was more than 10 mins ago, but we've had an invalid block since then
// Assume that the chain has stalled because there is no alternative valid candidate
// Enter recovery mode to allow alternative, valid candidates to be minted
recoverInvalidBlock = true;
}
parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
}
}
// If less than 30 seconds has passed since first detection the higher weight chain,
// we should skip our block submission to give us the opportunity to sync to the better chain
if (NTP.getTime() - timeOfLastLowWeightBlock < 30*1000L) {
LOGGER.debug("Higher weight chain found in peers, so not signing a block this round");
LOGGER.debug("Time since detected: {}ms", NTP.getTime() - timeOfLastLowWeightBlock);
// If our latest block isn't recent then we need to synchronize instead of minting, unless we're in recovery mode.
if (!peers.isEmpty() && lastBlockData.getTimestamp() < minLatestBlockTimestamp)
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
continue;
// There are enough peers with a recent block and our latest block is recent
// so go ahead and mint a block if possible.
isMintingPossible = true;
// Check blockchain hasn't changed
if (previousBlockData == null || !Arrays.equals(previousBlockData.getSignature(), lastBlockData.getSignature())) {
previousBlockData = lastBlockData;
newBlocks.clear();
// Reduce log timeout
logTimeout = 10 * 1000L;
// Last low weight block is no longer valid
parentSignatureForLastLowWeightBlock = null;
}
// Discard accounts we have already built blocks with
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
// Do we need to build any potential new blocks?
List<PrivateKeyAccount> newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
// We might need to sit the next block out, if one of our minting accounts signed the previous one
// Skip this check for single node testnets, since they definitely need to mint every block
byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
if (mintedLastBlock && !isSingleNodeTestnet) {
LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
continue;
}
if (parentSignatureForLastLowWeightBlock != null) {
// The last iteration found a higher weight block in the network, so sleep for a while
// to allow is to sync the higher weight chain. We are sleeping here rather than when
// detected as we don't want to hold the blockchain lock open.
LOGGER.info("Sleeping for 10 seconds...");
Thread.sleep(10 * 1000L);
}
for (PrivateKeyAccount mintingAccount : newBlocksMintingAccounts) {
// First block does the AT heavy-lifting
if (newBlocks.isEmpty()) {
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
if (newBlock == null) {
// For some reason we can't mint right now
moderatedLog(() -> LOGGER.info("Couldn't build a to-be-minted block"));
continue;
}
else {
// More than 30 seconds have passed, so we should submit our block candidate anyway.
LOGGER.debug("More than 30 seconds passed, so proceeding to submit block candidate...");
newBlocks.add(newBlock);
} else {
// The blocks for other minters require less effort...
Block newBlock = newBlocks.get(0).remint(mintingAccount);
if (newBlock == null) {
// For some reason we can't mint right now
moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
continue;
}
newBlocks.add(newBlock);
}
else {
LOGGER.debug("No higher weight chain found in peers");
}
} catch (DataException e) {
LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
}
// Discard any uncommitted changes as a result of the higher weight chain detection
repository.discardChanges();
// No potential block candidates?
if (newBlocks.isEmpty())
continue;
// Clear variables that track low weight blocks
parentSignatureForLastLowWeightBlock = null;
timeOfLastLowWeightBlock = null;
// Add unconfirmed transactions
addUnconfirmedTransactions(repository, newBlock);
// Sign to create block's signature
newBlock.sign();
// Is newBlock still valid?
ValidationResult validationResult = newBlock.isValid();
if (validationResult != ValidationResult.OK) {
// No longer valid? Report and discard
LOGGER.error(String.format("To-be-minted block now invalid '%s' after adding unconfirmed transactions?", validationResult.name()));
// Rebuild block candidates, just to be sure
newBlocks.clear();
// Make sure we're the only thread modifying the blockchain
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
LOGGER.debug("Couldn't acquire blockchain lock even after waiting 30 seconds");
continue;
}
// Add to blockchain - something else will notice and broadcast new block to network
boolean newBlockMinted = false;
Block newBlock = null;
try {
newBlock.process();
// Clear repository session state so we have latest view of data
repository.discardChanges();
repository.saveChanges();
// Now that we have blockchain lock, do final check that chain hasn't changed
BlockData latestBlockData = blockRepository.getLastBlock();
if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
continue;
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
List<Block> goodBlocks = new ArrayList<>();
boolean wasInvalidBlockDiscarded = false;
Iterator<Block> newBlocksIterator = newBlocks.iterator();
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
while (newBlocksIterator.hasNext()) {
Block testBlock = newBlocksIterator.next();
if (rewardShareData != null) {
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
newBlock.getBlockData().getHeight(),
Base58.encode(newBlock.getBlockData().getSignature()),
Base58.encode(newBlock.getParent().getSignature()),
rewardShareData.getMinter(),
rewardShareData.getRecipient()));
} else {
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
newBlock.getBlockData().getHeight(),
Base58.encode(newBlock.getBlockData().getSignature()),
Base58.encode(newBlock.getParent().getSignature()),
newBlock.getMinter().getAddress()));
// Is new block's timestamp valid yet?
// We do a separate check as some timestamp checks are skipped for testchains
if (testBlock.isTimestampValid() != ValidationResult.OK)
continue;
testBlock.preProcess();
// Is new block valid yet? (Before adding unconfirmed transactions)
ValidationResult result = testBlock.isValid();
if (result != ValidationResult.OK) {
moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
newBlocksIterator.remove();
wasInvalidBlockDiscarded = true;
/*
* Bail out fast so that we loop around from the top again.
* This gives BlockMinter the possibility to remint this candidate block using another block from newBlocks,
* via the Blocks.remint() method, which avoids having to re-process Block ATs all over again.
* Particularly useful if some aspect of Blocks changes due a timestamp-based feature-trigger (see BlockChain class).
*/
break;
}
goodBlocks.add(testBlock);
}
// Notify network after we're released blockchain lock
newBlockMinted = true;
if (wasInvalidBlockDiscarded || goodBlocks.isEmpty())
continue;
// Notify Controller
repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onNewBlock(newBlock.getBlockData());
} catch (DataException e) {
// Unable to process block - report and discard
LOGGER.error("Unable to process newly minted block?", e);
newBlocks.clear();
// Pick best block
final int parentHeight = previousBlockData.getHeight();
final byte[] parentBlockSignature = previousBlockData.getSignature();
BigInteger bestWeight = null;
for (int bi = 0; bi < goodBlocks.size(); ++bi) {
BlockData blockData = goodBlocks.get(bi).getBlockData();
BlockSummaryData blockSummaryData = new BlockSummaryData(blockData);
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
blockSummaryData.setMinterLevel(minterLevel);
BigInteger blockWeight = Block.calcBlockWeight(parentHeight, parentBlockSignature, blockSummaryData);
if (bestWeight == null || blockWeight.compareTo(bestWeight) < 0) {
newBlock = goodBlocks.get(bi);
bestWeight = blockWeight;
}
}
try {
if (this.higherWeightChainExists(repository, bestWeight)) {
// Check if the base block has updated since the last time we were here
if (parentSignatureForLastLowWeightBlock == null || timeOfLastLowWeightBlock == null ||
!Arrays.equals(parentSignatureForLastLowWeightBlock, previousBlockData.getSignature())) {
// We've switched to a different chain, so reset the timer
timeOfLastLowWeightBlock = NTP.getTime();
}
parentSignatureForLastLowWeightBlock = previousBlockData.getSignature();
// If less than 30 seconds has passed since first detection the higher weight chain,
// we should skip our block submission to give us the opportunity to sync to the better chain
if (NTP.getTime() - timeOfLastLowWeightBlock < 30 * 1000L) {
LOGGER.info("Higher weight chain found in peers, so not signing a block this round");
LOGGER.info("Time since detected: {}", NTP.getTime() - timeOfLastLowWeightBlock);
continue;
} else {
// More than 30 seconds have passed, so we should submit our block candidate anyway.
LOGGER.info("More than 30 seconds passed, so proceeding to submit block candidate...");
}
} else {
LOGGER.debug("No higher weight chain found in peers");
}
} catch (DataException e) {
LOGGER.debug("Unable to check for a higher weight chain. Proceeding anyway...");
}
// Discard any uncommitted changes as a result of the higher weight chain detection
repository.discardChanges();
// Clear variables that track low weight blocks
parentSignatureForLastLowWeightBlock = null;
timeOfLastLowWeightBlock = null;
// Add unconfirmed transactions
addUnconfirmedTransactions(repository, newBlock);
// Sign to create block's signature
newBlock.sign();
// Is newBlock still valid?
ValidationResult validationResult = newBlock.isValid();
if (validationResult != ValidationResult.OK) {
// No longer valid? Report and discard
LOGGER.error(String.format("To-be-minted block now invalid '%s' after adding unconfirmed transactions?", validationResult.name()));
// Rebuild block candidates, just to be sure
newBlocks.clear();
continue;
}
// Add to blockchain - something else will notice and broadcast new block to network
try {
newBlock.process();
repository.saveChanges();
LOGGER.info(String.format("Minted new block: %d", newBlock.getBlockData().getHeight()));
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
if (rewardShareData != null) {
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s on behalf of %s",
newBlock.getBlockData().getHeight(),
Base58.encode(newBlock.getBlockData().getSignature()),
Base58.encode(newBlock.getParent().getSignature()),
rewardShareData.getMinter(),
rewardShareData.getRecipient()));
} else {
LOGGER.info(String.format("Minted block %d, sig %.8s, parent sig: %.8s by %s",
newBlock.getBlockData().getHeight(),
Base58.encode(newBlock.getBlockData().getSignature()),
Base58.encode(newBlock.getParent().getSignature()),
newBlock.getMinter().getAddress()));
}
// Notify network after we're released blockchain lock
newBlockMinted = true;
// Notify Controller
repository.discardChanges(); // clear transaction status to prevent deadlocks
Controller.getInstance().onNewBlock(newBlock.getBlockData());
} catch (DataException e) {
// Unable to process block - report and discard
LOGGER.error("Unable to process newly minted block?", e);
newBlocks.clear();
}
} finally {
blockchainLock.unlock();
}
} finally {
blockchainLock.unlock();
}
if (newBlockMinted) {
// Broadcast our new chain to network
BlockData newBlockData = newBlock.getBlockData();
if (newBlockMinted) {
// Broadcast our new chain to network
Network.getInstance().broadcastOurChain();
}
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
}
}
} catch (DataException e) {
LOGGER.warn("Repository issue while running block minter", e);
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
LOGGER.warn("Repository issue while running block minter - NO LONGER MINTING", e);
}
}
@@ -552,18 +580,23 @@ public class BlockMinter extends Thread {
// This peer has common block data
CommonBlockData commonBlockData = peer.getCommonBlockData();
BlockSummaryData commonBlockSummaryData = commonBlockData.getCommonBlockSummary();
if (commonBlockData.getChainWeight() != null) {
if (commonBlockData.getChainWeight() != null && peer.getCommonBlockData().getBlockSummariesAfterCommonBlock() != null) {
// The synchronizer has calculated this peer's chain weight
BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
BigInteger peerChainWeight = commonBlockData.getChainWeight();
if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
// This peer has a higher weight chain than ours
LOGGER.debug("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
return true;
if (!Synchronizer.getInstance().containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
// .. and it doesn't hold any invalid blocks
BigInteger ourChainWeightSinceCommonBlock = this.getOurChainWeightSinceBlock(repository, commonBlockSummaryData, commonBlockData.getBlockSummariesAfterCommonBlock());
BigInteger ourChainWeight = ourChainWeightSinceCommonBlock.add(blockCandidateWeight);
BigInteger peerChainWeight = commonBlockData.getChainWeight();
if (peerChainWeight.compareTo(ourChainWeight) >= 0) {
// This peer has a higher weight chain than ours
LOGGER.info("Peer {} is on a higher weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
return true;
} else {
LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
}
} else {
LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
LOGGER.debug("Peer {} has an invalid block", peer);
}
} else {
LOGGER.debug("Peer {} has no chain weight", peer);

View File

@@ -32,6 +32,7 @@ import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.api.ApiService;
import org.qortal.api.DomainMapService;
import org.qortal.api.GatewayService;
import org.qortal.api.resource.TransactionsResource;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockChain.BlockTimingByHeight;
@@ -39,9 +40,11 @@ import org.qortal.controller.arbitrary.*;
import org.qortal.controller.repository.PruneManager;
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.naming.NameData;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
@@ -109,6 +112,7 @@ public class Controller extends Thread {
private long repositoryBackupTimestamp = startTime; // ms
private long repositoryMaintenanceTimestamp = startTime; // ms
private long repositoryCheckpointTimestamp = startTime; // ms
private long prunePeersTimestamp = startTime; // ms
private long ntpCheckTimestamp = startTime; // ms
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
@@ -179,6 +183,52 @@ public class Controller extends Thread {
}
public GetArbitraryMetadataMessageStats getArbitraryMetadataMessageStats = new GetArbitraryMetadataMessageStats();
public static class GetAccountMessageStats {
public AtomicLong requests = new AtomicLong();
public AtomicLong cacheHits = new AtomicLong();
public AtomicLong unknownAccounts = new AtomicLong();
public GetAccountMessageStats() {
}
}
public GetAccountMessageStats getAccountMessageStats = new GetAccountMessageStats();
public static class GetAccountBalanceMessageStats {
public AtomicLong requests = new AtomicLong();
public AtomicLong unknownAccounts = new AtomicLong();
public GetAccountBalanceMessageStats() {
}
}
public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats();
public static class GetAccountTransactionsMessageStats {
public AtomicLong requests = new AtomicLong();
public AtomicLong unknownAccounts = new AtomicLong();
public GetAccountTransactionsMessageStats() {
}
}
public GetAccountTransactionsMessageStats getAccountTransactionsMessageStats = new GetAccountTransactionsMessageStats();
public static class GetAccountNamesMessageStats {
public AtomicLong requests = new AtomicLong();
public AtomicLong unknownAccounts = new AtomicLong();
public GetAccountNamesMessageStats() {
}
}
public GetAccountNamesMessageStats getAccountNamesMessageStats = new GetAccountNamesMessageStats();
public static class GetNameMessageStats {
public AtomicLong requests = new AtomicLong();
public AtomicLong unknownAccounts = new AtomicLong();
public GetNameMessageStats() {
}
}
public GetNameMessageStats getNameMessageStats = new GetNameMessageStats();
public AtomicLong latestBlocksCacheRefills = new AtomicLong();
public StatsSnapshot() {
@@ -266,6 +316,10 @@ public class Controller extends Thread {
}
}
public static long uptime() {
return System.currentTimeMillis() - Controller.startTime;
}
/** Returns highest block, or null if it's not available. */
public BlockData getChainTip() {
synchronized (this.latestBlocks) {
@@ -363,23 +417,27 @@ public class Controller extends Thread {
return; // Not System.exit() so that GUI can display error
}
// Rebuild Names table and check database integrity (if enabled)
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
namesDatabaseIntegrityCheck.rebuildAllNames();
if (Settings.getInstance().isNamesIntegrityCheckEnabled()) {
namesDatabaseIntegrityCheck.runIntegrityCheck();
}
// If we have a non-lite node, we need to perform some startup actions
if (!Settings.getInstance().isLite()) {
LOGGER.info("Validating blockchain");
try {
BlockChain.validate();
// Rebuild Names table and check database integrity (if enabled)
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
namesDatabaseIntegrityCheck.rebuildAllNames();
if (Settings.getInstance().isNamesIntegrityCheckEnabled()) {
namesDatabaseIntegrityCheck.runIntegrityCheck();
}
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);
return; // Not System.exit() so that GUI can display error
LOGGER.info("Validating blockchain");
try {
BlockChain.validate();
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);
return; // Not System.exit() so that GUI can display error
}
}
// Import current trade bot states and minting accounts if they exist
@@ -442,6 +500,9 @@ public class Controller extends Thread {
AutoUpdate.getInstance().start();
}
LOGGER.info("Starting wallets");
PirateChainWalletController.getInstance().start();
LOGGER.info(String.format("Starting API on port %d", Settings.getInstance().getApiPort()));
try {
ApiService apiService = ApiService.getInstance();
@@ -498,6 +559,7 @@ public class Controller extends Thread {
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
long repositoryMaintenanceInterval = getRandomRepositoryMaintenanceInterval();
final long prunePeersInterval = 5 * 60 * 1000L; // Every 5 minutes
// Start executor service for trimming or pruning
PruneManager.getInstance().start();
@@ -595,10 +657,15 @@ public class Controller extends Thread {
}
// Prune stuck/slow/old peers
try {
Network.getInstance().prunePeers();
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
if (now >= prunePeersTimestamp + prunePeersInterval) {
prunePeersTimestamp = now + prunePeersInterval;
try {
LOGGER.debug("Pruning peers...");
Network.getInstance().prunePeers();
} catch (DataException e) {
LOGGER.warn(String.format("Repository issue when trying to prune peers: %s", e.getMessage()));
}
}
// Delete expired transactions
@@ -663,25 +730,25 @@ public class Controller extends Thread {
public static final Predicate<Peer> hasNoRecentBlock = peer -> {
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
final PeerChainTipData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp;
final BlockSummaryData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp;
};
public static final Predicate<Peer> hasNoOrSameBlock = peer -> {
final BlockData latestBlockData = getInstance().getChainTip();
final PeerChainTipData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getLastBlockSignature());
final BlockSummaryData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getSignature());
};
public static final Predicate<Peer> hasOnlyGenesisBlock = peer -> {
final PeerChainTipData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getLastHeight() == null || peerChainTipData.getLastHeight() == 1;
final BlockSummaryData peerChainTipData = peer.getChainTipData();
return peerChainTipData == null || peerChainTipData.getHeight() == 1;
};
public static final Predicate<Peer> hasInferiorChainTip = peer -> {
final PeerChainTipData peerChainTipData = peer.getChainTipData();
final BlockSummaryData peerChainTipData = peer.getChainTipData();
final List<ByteArray> inferiorChainTips = Synchronizer.getInstance().inferiorChainSignatures;
return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getLastBlockSignature()));
return peerChainTipData == null || peerChainTipData.getSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getSignature()));
};
public static final Predicate<Peer> hasOldVersion = peer -> {
@@ -733,19 +800,24 @@ public class Controller extends Thread {
String actionText;
// Use a more tolerant latest block timestamp in the isUpToDate() calls below to reduce misleading statuses.
// Any block in the last 30 minutes is considered "up to date" for the purposes of displaying statuses.
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
// Any block in the last 2 hours is considered "up to date" for the purposes of displaying statuses.
// This also aligns with the time interval required for continued online account submission.
final Long minLatestBlockTimestamp = NTP.getTime() - (2 * 60 * 60 * 1000L);
// Only show sync percent if it's less than 100, to avoid confusion
final Integer syncPercent = Synchronizer.getInstance().getSyncPercent();
final boolean isSyncing = (syncPercent != null && syncPercent < 100);
synchronized (Synchronizer.getInstance().syncLock) {
if (this.isMintingPossible) {
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
SysTray.getInstance().setTrayIcon(2);
if (Settings.getInstance().isLite()) {
actionText = Translator.INSTANCE.translate("SysTray", "LITE_NODE");
SysTray.getInstance().setTrayIcon(4);
}
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers()) {
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
SysTray.getInstance().setTrayIcon(3);
}
else if (!this.isUpToDate(minLatestBlockTimestamp) && Synchronizer.getInstance().isSynchronizing()) {
else if (!this.isUpToDate(minLatestBlockTimestamp) && isSyncing) {
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), Synchronizer.getInstance().getSyncPercent());
SysTray.getInstance().setTrayIcon(3);
}
@@ -753,13 +825,27 @@ public class Controller extends Thread {
actionText = String.format("%s", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"));
SysTray.getInstance().setTrayIcon(3);
}
else if (OnlineAccountsManager.getInstance().hasActiveOnlineAccountSignatures()) {
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
SysTray.getInstance().setTrayIcon(2);
}
else {
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
SysTray.getInstance().setTrayIcon(4);
}
}
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height) + "\n" + String.format("%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion);
String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText);
if (!Settings.getInstance().isLite()) {
tooltip = tooltip.concat(String.format(" - %s %d", heightText, height));
final Integer blocksRemaining = Synchronizer.getInstance().getBlocksRemaining();
if (blocksRemaining != null && blocksRemaining > 0) {
String blocksRemainingText = Translator.INSTANCE.translate("SysTray", "BLOCKS_REMAINING");
tooltip = tooltip.concat(String.format(" - %d %s", blocksRemaining, blocksRemainingText));
}
}
tooltip = tooltip.concat(String.format("\n%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion));
SysTray.getInstance().setToolTipText(tooltip);
this.callbackExecutor.execute(() -> {
@@ -816,6 +902,9 @@ public class Controller extends Thread {
LOGGER.info("Shutting down API");
ApiService.getInstance().stop();
LOGGER.info("Shutting down wallets");
PirateChainWalletController.getInstance().shutdown();
if (Settings.getInstance().isAutoUpdateEnabled()) {
LOGGER.info("Shutting down auto-update");
AutoUpdate.getInstance().shutdown();
@@ -916,14 +1005,18 @@ public class Controller extends Thread {
// Callbacks for/from network
public void doNetworkBroadcast() {
if (Settings.getInstance().isLite()) {
// Lite nodes have nothing to broadcast
return;
}
Network network = Network.getInstance();
// Send (if outbound) / Request peer lists
network.broadcast(peer -> peer.isOutbound() ? network.buildPeersMessage(peer) : new GetPeersMessage());
// Send our current height
BlockData latestBlockData = getChainTip();
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
network.broadcastOurChain();
// Request unconfirmed transaction signatures, but only if we're up-to-date.
// If we're NOT up-to-date then priority is synchronizing first
@@ -1130,6 +1223,10 @@ public class Controller extends Thread {
onNetworkHeightV2Message(peer, message);
break;
case BLOCK_SUMMARIES_V2:
onNetworkBlockSummariesV2Message(peer, message);
break;
case GET_TRANSACTION:
TransactionImporter.getInstance().onNetworkGetTransactionMessage(peer, message);
break;
@@ -1147,19 +1244,18 @@ public class Controller extends Thread {
break;
case GET_ONLINE_ACCOUNTS:
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsMessage(peer, message);
break;
case ONLINE_ACCOUNTS:
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsMessage(peer, message);
break;
case GET_ONLINE_ACCOUNTS_V2:
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV2Message(peer, message);
case ONLINE_ACCOUNTS_V2:
// No longer supported - to be eventually removed
break;
case ONLINE_ACCOUNTS_V2:
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV2Message(peer, message);
case GET_ONLINE_ACCOUNTS_V3:
OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV3Message(peer, message);
break;
case ONLINE_ACCOUNTS_V3:
OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV3Message(peer, message);
break;
case GET_ARBITRARY_DATA:
@@ -1198,6 +1294,26 @@ public class Controller extends Thread {
TradeBot.getInstance().onTradePresencesMessage(peer, message);
break;
case GET_ACCOUNT:
onNetworkGetAccountMessage(peer, message);
break;
case GET_ACCOUNT_BALANCE:
onNetworkGetAccountBalanceMessage(peer, message);
break;
case GET_ACCOUNT_TRANSACTIONS:
onNetworkGetAccountTransactionsMessage(peer, message);
break;
case GET_ACCOUNT_NAMES:
onNetworkGetAccountNamesMessage(peer, message);
break;
case GET_NAME:
onNetworkGetNameMessage(peer, message);
break;
default:
LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
break;
@@ -1265,8 +1381,10 @@ public class Controller extends Thread {
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
// We'll send empty block summaries message as it's very short
Message blockUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
// Send generic 'unknown' message as it's very short
Message blockUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
? new GenericUnknownMessage()
: new BlockSummariesMessage(Collections.emptyList());
blockUnknownMessage.setId(message.getId());
if (!peer.sendMessage(blockUnknownMessage))
peer.disconnect("failed to send block-unknown response");
@@ -1275,6 +1393,18 @@ public class Controller extends Thread {
Block block = new Block(repository, blockData);
// V2 support
if (peer.getPeersVersion() >= BlockV2Message.MIN_PEER_VERSION) {
Message blockMessage = new BlockV2Message(block);
blockMessage.setId(message.getId());
if (!peer.sendMessage(blockMessage)) {
peer.disconnect("failed to send block");
// Don't fall-through to caching because failure to send might be from failure to build message
return;
}
return;
}
CachedBlockMessage blockMessage = new CachedBlockMessage(block);
blockMessage.setId(message.getId());
@@ -1303,11 +1433,15 @@ public class Controller extends Thread {
this.stats.getBlockSummariesStats.requests.incrementAndGet();
// If peer's parent signature matches our latest block signature
// then we can short-circuit with an empty response
// then we have no blocks after that and can short-circuit with an empty response
BlockData chainTip = getChainTip();
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
? new BlockSummariesV2Message(Collections.emptyList())
: new BlockSummariesMessage(Collections.emptyList());
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
@@ -1363,7 +1497,9 @@ public class Controller extends Thread {
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
}
Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
? new BlockSummariesV2Message(blockSummaries)
: new BlockSummariesMessage(blockSummaries);
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
@@ -1434,20 +1570,250 @@ public class Controller extends Thread {
private void onNetworkHeightV2Message(Peer peer, Message message) {
HeightV2Message heightV2Message = (HeightV2Message) message;
// If peer is inbound and we've not updated their height
// then this is probably their initial HEIGHT_V2 message
// so they need a corresponding HEIGHT_V2 message from us
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
if (!Settings.getInstance().isLite()) {
// If peer is inbound and we've not updated their height
// then this is probably their initial HEIGHT_V2 message
// so they need a corresponding HEIGHT_V2 message from us
if (!peer.isOutbound() && peer.getChainTipData() == null) {
Message responseMessage = Network.getInstance().buildHeightOrChainTipInfo(peer);
if (responseMessage == null || !peer.sendMessage(responseMessage)) {
peer.disconnect("failed to send our chain tip info");
return;
}
}
}
// Update peer chain tip data
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
BlockSummaryData newChainTipData = new BlockSummaryData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getMinterPublicKey(), heightV2Message.getTimestamp());
peer.setChainTipData(newChainTipData);
// Potentially synchronize
Synchronizer.getInstance().requestSync();
}
private void onNetworkBlockSummariesV2Message(Peer peer, Message message) {
BlockSummariesV2Message blockSummariesV2Message = (BlockSummariesV2Message) message;
if (!Settings.getInstance().isLite()) {
// If peer is inbound and we've not updated their height
// then this is probably their initial BLOCK_SUMMARIES_V2 message
// so they need a corresponding BLOCK_SUMMARIES_V2 message from us
if (!peer.isOutbound() && peer.getChainTipData() == null) {
Message responseMessage = Network.getInstance().buildHeightOrChainTipInfo(peer);
if (responseMessage == null || !peer.sendMessage(responseMessage)) {
peer.disconnect("failed to send our chain tip info");
return;
}
}
}
if (message.hasId()) {
/*
* Experimental proof-of-concept: discard messages with ID
* These are 'late' reply messages received after timeout has expired,
* having been passed upwards from Peer to Network to Controller.
* Hence, these are NOT simple "here's my chain tip" broadcasts from other peers.
*/
LOGGER.debug("Discarding late {} message with ID {} from {}", message.getType().name(), message.getId(), peer);
return;
}
// Update peer chain tip data
peer.setChainTipSummaries(blockSummariesV2Message.getBlockSummaries());
// Potentially synchronize
Synchronizer.getInstance().requestSync();
}
private void onNetworkGetAccountMessage(Peer peer, Message message) {
GetAccountMessage getAccountMessage = (GetAccountMessage) message;
String address = getAccountMessage.getAddress();
this.stats.getAccountMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
if (accountData == null) {
// We don't have this account
this.stats.getAccountMessageStats.unknownAccounts.getAndIncrement();
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s", peer, address));
// Send generic 'unknown' message as it's very short
Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
if (!peer.sendMessage(accountUnknownMessage))
peer.disconnect("failed to send account-unknown response");
return;
}
AccountMessage accountMessage = new AccountMessage(accountData);
accountMessage.setId(message.getId());
if (!peer.sendMessage(accountMessage)) {
peer.disconnect("failed to send account");
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send account %s to peer %s", address, peer), e);
}
}
private void onNetworkGetAccountBalanceMessage(Peer peer, Message message) {
GetAccountBalanceMessage getAccountBalanceMessage = (GetAccountBalanceMessage) message;
String address = getAccountBalanceMessage.getAddress();
long assetId = getAccountBalanceMessage.getAssetId();
this.stats.getAccountBalanceMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(address, assetId);
if (accountBalanceData == null) {
// We don't have this account
this.stats.getAccountBalanceMessageStats.unknownAccounts.getAndIncrement();
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_BALANCE request for unknown account %s and asset ID %d", peer, address, assetId));
// Send generic 'unknown' message as it's very short
Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
if (!peer.sendMessage(accountUnknownMessage))
peer.disconnect("failed to send account-unknown response");
return;
}
AccountBalanceMessage accountMessage = new AccountBalanceMessage(accountBalanceData);
accountMessage.setId(message.getId());
if (!peer.sendMessage(accountMessage)) {
peer.disconnect("failed to send account balance");
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send balance for account %s and asset ID %d to peer %s", address, assetId, peer), e);
}
}
private void onNetworkGetAccountTransactionsMessage(Peer peer, Message message) {
GetAccountTransactionsMessage getAccountTransactionsMessage = (GetAccountTransactionsMessage) message;
String address = getAccountTransactionsMessage.getAddress();
int limit = Math.min(getAccountTransactionsMessage.getLimit(), 100);
int offset = getAccountTransactionsMessage.getOffset();
this.stats.getAccountTransactionsMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null,
null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, false);
// Expand signatures to transactions
List<TransactionData> transactions = new ArrayList<>(signatures.size());
for (byte[] signature : signatures) {
transactions.add(repository.getTransactionRepository().fromSignature(signature));
}
if (transactions == null) {
// We don't have this account
this.stats.getAccountTransactionsMessageStats.unknownAccounts.getAndIncrement();
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_TRANSACTIONS request for unknown account %s", peer, address));
// Send generic 'unknown' message as it's very short
Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
if (!peer.sendMessage(accountUnknownMessage))
peer.disconnect("failed to send account-unknown response");
return;
}
TransactionsMessage transactionsMessage = new TransactionsMessage(transactions);
transactionsMessage.setId(message.getId());
if (!peer.sendMessage(transactionsMessage)) {
peer.disconnect("failed to send account transactions");
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while sending transactions for account %s %d to peer %s", address, peer), e);
} catch (MessageException e) {
LOGGER.error(String.format("Message serialization issue while sending transactions for account %s %d to peer %s", address, peer), e);
}
}
private void onNetworkGetAccountNamesMessage(Peer peer, Message message) {
GetAccountNamesMessage getAccountNamesMessage = (GetAccountNamesMessage) message;
String address = getAccountNamesMessage.getAddress();
this.stats.getAccountNamesMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
List<NameData> namesDataList = repository.getNameRepository().getNamesByOwner(address);
if (namesDataList == null) {
// We don't have this account
this.stats.getAccountNamesMessageStats.unknownAccounts.getAndIncrement();
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_NAMES request for unknown account %s", peer, address));
// Send generic 'unknown' message as it's very short
Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
if (!peer.sendMessage(accountUnknownMessage))
peer.disconnect("failed to send account-unknown response");
return;
}
NamesMessage namesMessage = new NamesMessage(namesDataList);
namesMessage.setId(message.getId());
if (!peer.sendMessage(namesMessage)) {
peer.disconnect("failed to send account names");
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send names for account %s to peer %s", address, peer), e);
}
}
private void onNetworkGetNameMessage(Peer peer, Message message) {
GetNameMessage getNameMessage = (GetNameMessage) message;
String name = getNameMessage.getName();
this.stats.getNameMessageStats.requests.incrementAndGet();
try (final Repository repository = RepositoryManager.getRepository()) {
NameData nameData = repository.getNameRepository().fromName(name);
if (nameData == null) {
// We don't have this account
this.stats.getNameMessageStats.unknownAccounts.getAndIncrement();
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'name unknown' response to peer %s for GET_NAME request for unknown name %s", peer, name));
// Send generic 'unknown' message as it's very short
Message nameUnknownMessage = new GenericUnknownMessage();
nameUnknownMessage.setId(message.getId());
if (!peer.sendMessage(nameUnknownMessage))
peer.disconnect("failed to send name-unknown response");
return;
}
NamesMessage namesMessage = new NamesMessage(Arrays.asList(nameData));
namesMessage.setId(message.getId());
if (!peer.sendMessage(namesMessage)) {
peer.disconnect("failed to send name data");
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send name %s to peer %s", name, peer), e);
}
}
// Utilities
@@ -1477,14 +1843,14 @@ public class Controller extends Thread {
continue;
}
final PeerChainTipData peerChainTipData = peer.getChainTipData();
BlockSummaryData peerChainTipData = peer.getChainTipData();
if (peerChainTipData == null) {
iterator.remove();
continue;
}
// Disregard peers that don't have a recent block
if (peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp) {
if (peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp) {
iterator.remove();
continue;
}
@@ -1499,6 +1865,11 @@ public class Controller extends Thread {
* @return boolean - whether our node's blockchain is up to date or not
*/
public boolean isUpToDate(Long minLatestBlockTimestamp) {
if (Settings.getInstance().isLite()) {
// Lite nodes are always "up to date"
return true;
}
// Do we even have a vaguely recent block?
if (minLatestBlockTimestamp == null)
return false;
@@ -1507,6 +1878,10 @@ public class Controller extends Thread {
if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp)
return false;
if (Settings.getInstance().isSingleNodeTestnet())
// Single node testnets won't have peers, so we can assume up to date from this point
return true;
// Needs a mutable copy of the unmodifiableList
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
if (peers == null)

View File

@@ -0,0 +1,189 @@
package org.qortal.controller;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.naming.NameData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.message.*;
import java.security.SecureRandom;
import java.util.*;
import static org.qortal.network.message.MessageType.*;
public class LiteNode {
private static final Logger LOGGER = LogManager.getLogger(LiteNode.class);
private static LiteNode instance;
public Map<Integer, Long> pendingRequests = Collections.synchronizedMap(new HashMap<>());
public int MAX_TRANSACTIONS_PER_MESSAGE = 100;
public LiteNode() {
}
public static synchronized LiteNode getInstance() {
if (instance == null) {
instance = new LiteNode();
}
return instance;
}
/**
* Fetch account data from peers for given QORT address
* @param address - the QORT address to query
* @return accountData - the account data for this address, or null if not retrieved
*/
public AccountData fetchAccountData(String address) {
GetAccountMessage getAccountMessage = new GetAccountMessage(address);
AccountMessage accountMessage = (AccountMessage) this.sendMessage(getAccountMessage, ACCOUNT);
if (accountMessage == null) {
return null;
}
return accountMessage.getAccountData();
}
/**
* Fetch account balance data from peers for given QORT address and asset ID
* @param address - the QORT address to query
* @return balance - the balance for this address and assetId, or null if not retrieved
*/
public AccountBalanceData fetchAccountBalance(String address, long assetId) {
GetAccountBalanceMessage getAccountMessage = new GetAccountBalanceMessage(address, assetId);
AccountBalanceMessage accountMessage = (AccountBalanceMessage) this.sendMessage(getAccountMessage, ACCOUNT_BALANCE);
if (accountMessage == null) {
return null;
}
return accountMessage.getAccountBalanceData();
}
/**
* Fetch list of transactions for given QORT address
* @param address - the QORT address to query
* @param limit - the maximum number of results to return
* @param offset - the starting index
* @return a list of TransactionData objects, or null if not retrieved
*/
public List<TransactionData> fetchAccountTransactions(String address, int limit, int offset) {
List<TransactionData> allTransactions = new ArrayList<>();
if (limit == 0) {
limit = Integer.MAX_VALUE;
}
int batchSize = Math.min(limit, MAX_TRANSACTIONS_PER_MESSAGE);
while (allTransactions.size() < limit) {
GetAccountTransactionsMessage getAccountTransactionsMessage = new GetAccountTransactionsMessage(address, batchSize, offset);
TransactionsMessage transactionsMessage = (TransactionsMessage) this.sendMessage(getAccountTransactionsMessage, TRANSACTIONS);
if (transactionsMessage == null) {
// An error occurred, so give up instead of returning partial results
return null;
}
allTransactions.addAll(transactionsMessage.getTransactions());
if (transactionsMessage.getTransactions().size() < batchSize) {
// No more transactions to fetch
break;
}
offset += batchSize;
}
return allTransactions;
}
/**
* Fetch list of names for given QORT address
* @param address - the QORT address to query
* @return a list of NameData objects, or null if not retrieved
*/
public List<NameData> fetchAccountNames(String address) {
GetAccountNamesMessage getAccountNamesMessage = new GetAccountNamesMessage(address);
NamesMessage namesMessage = (NamesMessage) this.sendMessage(getAccountNamesMessage, NAMES);
if (namesMessage == null) {
return null;
}
return namesMessage.getNameDataList();
}
/**
* Fetch info about a registered name
* @param name - the name to query
* @return a NameData object, or null if not retrieved
*/
public NameData fetchNameData(String name) {
GetNameMessage getNameMessage = new GetNameMessage(name);
NamesMessage namesMessage = (NamesMessage) this.sendMessage(getNameMessage, NAMES);
if (namesMessage == null) {
return null;
}
List<NameData> nameDataList = namesMessage.getNameDataList();
if (nameDataList == null || nameDataList.size() != 1) {
return null;
}
// We are only expecting a single item in the list
return nameDataList.get(0);
}
private Message sendMessage(Message message, MessageType expectedResponseMessageType) {
// This asks a random peer for the data
// TODO: ask multiple peers, and disregard everything if there are any significant differences in the responses
// Needs a mutable copy of the unmodifiableList
List<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
// Disregard peers that have "misbehaved" recently
peers.removeIf(Controller.hasMisbehaved);
// Disregard peers that only have genesis block
// TODO: peers.removeIf(Controller.hasOnlyGenesisBlock);
// Disregard peers that are on an old version
peers.removeIf(Controller.hasOldVersion);
// Disregard peers that are on a known inferior chain tip
// TODO: peers.removeIf(Controller.hasInferiorChainTip);
if (peers.isEmpty()) {
LOGGER.info("No peers available to send {} message to", message.getType());
return null;
}
// Pick random peer
int index = new SecureRandom().nextInt(peers.size());
Peer peer = peers.get(index);
LOGGER.info("Sending {} message to peer {}...", message.getType(), peer);
Message responseMessage;
try {
responseMessage = peer.getResponse(message);
} catch (InterruptedException e) {
return null;
}
if (responseMessage == null) {
LOGGER.info("Peer {} didn't respond to {} message", peer, message.getType());
return null;
}
else if (responseMessage.getType() != expectedResponseMessageType) {
LOGGER.info("Peer responded with unexpected message type {} (should be {})", peer, responseMessage.getType(), expectedResponseMessageType);
return null;
}
LOGGER.info("Peer {} responded with {} message", peer, responseMessage.getType());
return responseMessage;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,404 @@
package org.qortal.controller;
import com.rust.litewalletjni.LiteWalletJni;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataReader;
import org.qortal.arbitrary.ArbitraryDataResource;
import org.qortal.arbitrary.exception.MissingDataException;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.PirateWallet;
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.ArbitraryTransaction;
import org.qortal.utils.ArbitraryTransactionUtils;
import org.qortal.utils.Base58;
import org.qortal.utils.FilesystemUtils;
import org.qortal.utils.NTP;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
public class PirateChainWalletController extends Thread {
protected static final Logger LOGGER = LogManager.getLogger(PirateChainWalletController.class);
private static PirateChainWalletController instance;
final private static long SAVE_INTERVAL = 60 * 60 * 1000L; // 1 hour
private long lastSaveTime = 0L;
private boolean running;
private PirateWallet currentWallet = null;
private boolean shouldLoadWallet = false;
private String loadStatus = null;
private static String qdnWalletSignature = "EsfUw54perxkEtfoUoL7Z97XPrNsZRZXePVZPz3cwRm9qyEPSofD5KmgVpDqVitQp7LhnZRmL6z2V9hEe1YS45T";
private PirateChainWalletController() {
this.running = true;
}
public static PirateChainWalletController getInstance() {
if (instance == null)
instance = new PirateChainWalletController();
return instance;
}
@Override
public void run() {
Thread.currentThread().setName("Pirate Chain Wallet Controller");
try {
while (running && !Controller.isStopping()) {
Thread.sleep(1000);
// Wait until we have a request to load the wallet
if (!shouldLoadWallet) {
continue;
}
if (!LiteWalletJni.isLoaded()) {
this.loadLibrary();
// If still not loaded, sleep to prevent too many requests
if (!LiteWalletJni.isLoaded()) {
Thread.sleep(5 * 1000);
continue;
}
}
// Wallet is downloaded, so clear the status
this.loadStatus = null;
if (this.currentWallet == null) {
// Nothing to do yet
continue;
}
if (this.currentWallet.isNullSeedWallet()) {
// Don't sync the null seed wallet
continue;
}
LOGGER.debug("Syncing Pirate Chain wallet...");
String response = LiteWalletJni.execute("sync", "");
LOGGER.debug("sync response: {}", response);
try {
JSONObject json = new JSONObject(response);
if (json.has("result")) {
String result = json.getString("result");
// We may have to set wallet to ready if this is the first ever successful sync
if (Objects.equals(result, "success")) {
this.currentWallet.setReady(true);
}
}
} catch (JSONException e) {
LOGGER.info("Unable to interpret JSON", e);
}
// Rate limit sync attempts
Thread.sleep(30000);
// Save wallet if needed
Long now = NTP.getTime();
if (now != null && now-SAVE_INTERVAL >= this.lastSaveTime) {
this.saveCurrentWallet();
}
}
} catch (InterruptedException e) {
// Fall-through to exit
}
}
public void shutdown() {
// Save the wallet
this.saveCurrentWallet();
this.running = false;
this.interrupt();
}
// QDN & wallet libraries
private void loadLibrary() throws InterruptedException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Check if architecture is supported
String libFileName = PirateChainWalletController.getRustLibFilename();
if (libFileName == null) {
String osName = System.getProperty("os.name");
String osArchitecture = System.getProperty("os.arch");
this.loadStatus = String.format("Unsupported architecture (%s %s)", osName, osArchitecture);
return;
}
// Check if the library exists in the wallets folder
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
Path libPath = Paths.get(libDirectory.toString(), libFileName);
if (Files.exists(libPath)) {
// Already downloaded; we can load the library right away
LiteWalletJni.loadLibrary();
return;
}
// Library not found, so check if we've fetched the resource from QDN
ArbitraryTransactionData t = this.getTransactionData(repository);
if (t == null) {
// Can't find the transaction - maybe on a different chain?
return;
}
// Wait until we have a sufficient number of peers to attempt QDN downloads
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
if (handshakedPeers.size() < Settings.getInstance().getMinBlockchainPeers()) {
// Wait for more peers
this.loadStatus = String.format("Searching for peers...");
return;
}
// Build resource
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(t.getName(),
ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier());
try {
arbitraryDataReader.loadSynchronously(false);
} catch (MissingDataException e) {
LOGGER.info("Missing data when loading Pirate Chain library");
}
// Check its status
ArbitraryResourceStatus status = ArbitraryTransactionUtils.getStatus(
t.getService(), t.getName(), t.getIdentifier(), false);
if (status.getStatus() != ArbitraryResourceStatus.Status.READY) {
LOGGER.info("Not ready yet: {}", status.getTitle());
this.loadStatus = String.format("Downloading files from QDN... (%d / %d)", status.getLocalChunkCount(), status.getTotalChunkCount());
return;
}
// Files are downloaded, so copy the necessary files to the wallets folder
// Delete the wallets/*/lib directory first, in case earlier versions of the wallet are present
Path walletsLibDirectory = PirateChainWalletController.getWalletsLibDirectory();
if (Files.exists(walletsLibDirectory)) {
FilesystemUtils.safeDeleteDirectory(walletsLibDirectory, false);
}
Files.createDirectories(libDirectory);
FileUtils.copyDirectory(arbitraryDataReader.getFilePath().toFile(), libDirectory.toFile());
// Clear reader cache so only one copy exists
ArbitraryDataResource resource = new ArbitraryDataResource(t.getName(),
ArbitraryDataFile.ResourceIdType.NAME, t.getService(), t.getIdentifier());
resource.deleteCache();
// Finally, load the library
LiteWalletJni.loadLibrary();
} catch (DataException e) {
LOGGER.error("Repository issue when loading Pirate Chain library", e);
} catch (IOException e) {
LOGGER.error("Error when loading Pirate Chain library", e);
}
}
private ArbitraryTransactionData getTransactionData(Repository repository) {
try {
byte[] signature = Base58.decode(qdnWalletSignature);
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (!(transactionData instanceof ArbitraryTransactionData))
return null;
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
if (arbitraryTransaction != null) {
return (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
}
return null;
} catch (DataException e) {
return null;
}
}
public static String getRustLibFilename() {
String osName = System.getProperty("os.name");
String osArchitecture = System.getProperty("os.arch");
if (osName.equals("Mac OS X") && osArchitecture.equals("x86_64")) {
return "librust-macos-x86_64.dylib";
}
else if ((osName.equals("Linux") || osName.equals("FreeBSD")) && osArchitecture.equals("aarch64")) {
return "librust-linux-aarch64.so";
}
else if ((osName.equals("Linux") || osName.equals("FreeBSD")) && osArchitecture.equals("amd64")) {
return "librust-linux-x86_64.so";
}
else if (osName.contains("Windows") && osArchitecture.equals("amd64")) {
return "librust-windows-x86_64.dll";
}
return null;
}
public static Path getWalletsLibDirectory() {
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib");
}
public static Path getRustLibOuterDirectory() {
String sigPrefix = qdnWalletSignature.substring(0, 8);
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", "lib", sigPrefix);
}
// Wallet functions
public boolean initWithEntropy58(String entropy58) {
return this.initWithEntropy58(entropy58, false);
}
public boolean initNullSeedWallet() {
return this.initWithEntropy58(Base58.encode(new byte[32]), true);
}
private boolean initWithEntropy58(String entropy58, boolean isNullSeedWallet) {
// If the JNI library isn't loaded yet then we can't proceed
if (!LiteWalletJni.isLoaded()) {
shouldLoadWallet = true;
return false;
}
byte[] entropyBytes = Base58.decode(entropy58);
if (entropyBytes == null || entropyBytes.length != 32) {
LOGGER.info("Invalid entropy bytes");
return false;
}
if (this.currentWallet != null) {
if (this.currentWallet.entropyBytesEqual(entropyBytes)) {
// Wallet already active - nothing to do
return true;
}
else {
// Different wallet requested - close the existing one and switch over
this.closeCurrentWallet();
}
}
try {
this.currentWallet = new PirateWallet(entropyBytes, isNullSeedWallet);
if (!this.currentWallet.isReady()) {
// Don't persist wallets that aren't ready
this.currentWallet = null;
}
return true;
} catch (IOException e) {
LOGGER.info("Unable to initialize wallet: {}", e.getMessage());
}
return false;
}
private void saveCurrentWallet() {
if (this.currentWallet == null) {
// Nothing to do
return;
}
try {
if (this.currentWallet.save()) {
Long now = NTP.getTime();
if (now != null) {
this.lastSaveTime = now;
}
}
} catch (IOException e) {
LOGGER.info("Unable to save wallet");
}
}
public PirateWallet getCurrentWallet() {
return this.currentWallet;
}
private void closeCurrentWallet() {
this.saveCurrentWallet();
this.currentWallet = null;
}
public void ensureInitialized() throws ForeignBlockchainException {
if (!LiteWalletJni.isLoaded() || this.currentWallet == null || !this.currentWallet.isInitialized()) {
throw new ForeignBlockchainException("Pirate wallet isn't initialized yet");
}
}
public void ensureNotNullSeed() throws ForeignBlockchainException {
// Safety check to make sure funds aren't sent to a null seed wallet
if (this.currentWallet == null || this.currentWallet.isNullSeedWallet()) {
throw new ForeignBlockchainException("Invalid wallet");
}
}
public void ensureSynchronized() throws ForeignBlockchainException {
if (this.currentWallet == null || !this.currentWallet.isSynchronized()) {
throw new ForeignBlockchainException("Wallet isn't synchronized yet");
}
String response = LiteWalletJni.execute("syncStatus", "");
JSONObject json = new JSONObject(response);
if (json.has("syncing")) {
boolean isSyncing = Boolean.valueOf(json.getString("syncing"));
if (isSyncing) {
long syncedBlocks = json.getLong("synced_blocks");
long totalBlocks = json.getLong("total_blocks");
throw new ForeignBlockchainException(String.format("Sync in progress (%d / %d). Please try again later.", syncedBlocks, totalBlocks));
}
}
}
public String getSyncStatus() {
if (this.currentWallet == null || !this.currentWallet.isInitialized()) {
if (this.loadStatus != null) {
return this.loadStatus;
}
return "Not initialized yet";
}
String syncStatusResponse = LiteWalletJni.execute("syncStatus", "");
org.json.JSONObject json = new JSONObject(syncStatusResponse);
if (json.has("syncing")) {
boolean isSyncing = Boolean.valueOf(json.getString("syncing"));
if (isSyncing) {
long syncedBlocks = json.getLong("synced_blocks");
long totalBlocks = json.getLong("total_blocks");
return String.format("Sync in progress (%d / %d)", syncedBlocks, totalBlocks);
}
}
boolean isSynchronized = this.currentWallet.isSynchronized();
if (isSynchronized) {
return "Synchronized";
}
return "Initializing wallet...";
}
}

View File

@@ -19,21 +19,13 @@ import org.qortal.block.BlockChain;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.block.CommonBlockData;
import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.transaction.RewardShareTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.message.BlockMessage;
import org.qortal.network.message.BlockSummariesMessage;
import org.qortal.network.message.GetBlockMessage;
import org.qortal.network.message.GetBlockSummariesMessage;
import org.qortal.network.message.GetSignaturesV2Message;
import org.qortal.network.message.Message;
import org.qortal.network.message.SignaturesMessage;
import org.qortal.network.message.MessageType;
import org.qortal.network.message.*;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -61,7 +53,8 @@ public class Synchronizer extends Thread {
/** Maximum number of block signatures we ask from peer in one go */
private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings?
private static final long RECOVERY_MODE_TIMEOUT = 10 * 60 * 1000L; // ms
/** Maximum number of consecutive failed sync attempts before marking peer as misbehaved */
private static final int MAX_CONSECUTIVE_FAILED_SYNC_ATTEMPTS = 3;
private boolean running;
@@ -83,12 +76,14 @@ public class Synchronizer extends Thread {
private volatile boolean isSynchronizing = false;
/** Temporary estimate of synchronization progress for SysTray use. */
private volatile int syncPercent = 0;
/** Temporary estimate of blocks remaining for SysTray use. */
private volatile int blocksRemaining = 0;
private static volatile boolean requestSync = false;
private boolean syncRequestPending = false;
// Keep track of invalid blocks so that we don't keep trying to sync them
private Map<String, Long> invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
private Map<ByteArray, Long> invalidBlockSignatures = Collections.synchronizedMap(new HashMap<>());
public Long timeValidBlockLastReceived = null;
public Long timeInvalidBlockLastReceived = null;
@@ -134,6 +129,11 @@ public class Synchronizer extends Thread {
public void run() {
Thread.currentThread().setName("Synchronizer");
if (Settings.getInstance().isLite()) {
// Lite nodes don't need to sync
return;
}
try {
while (running && !Controller.isStopping()) {
Thread.sleep(1000);
@@ -173,8 +173,8 @@ public class Synchronizer extends Thread {
public Integer getSyncPercent() {
synchronized (this.syncLock) {
// Report as 100% synced if the latest block is within the last 30 mins
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
// Report as 100% synced if the latest block is within the last 60 mins
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
return 100;
}
@@ -183,6 +183,18 @@ public class Synchronizer extends Thread {
}
}
public Integer getBlocksRemaining() {
synchronized (this.syncLock) {
// Report as 0 blocks remaining if the latest block is within the last 60 mins
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
return 0;
}
return this.isSynchronizing ? this.blocksRemaining : null;
}
}
public void requestSync() {
requestSync = true;
}
@@ -284,7 +296,7 @@ public class Synchronizer extends Thread {
BlockData priorChainTip = Controller.getInstance().getChainTip();
synchronized (this.syncLock) {
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getHeight();
// Only update SysTray if we're potentially changing height
if (this.syncPercent < 100) {
@@ -314,7 +326,7 @@ public class Synchronizer extends Thread {
case INFERIOR_CHAIN: {
// Update our list of inferior chain tips
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getSignature());
if (!inferiorChainSignatures.contains(inferiorChainSignature))
inferiorChainSignatures.add(inferiorChainSignature);
@@ -322,7 +334,8 @@ public class Synchronizer extends Thread {
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
// Notify peer of our superior chain
if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, priorChainTip)))
Message message = Network.getInstance().buildHeightOrChainTipInfo(peer);
if (message == null || !peer.sendMessage(message))
peer.disconnect("failed to notify peer of our superior chain");
break;
}
@@ -343,7 +356,7 @@ public class Synchronizer extends Thread {
// fall-through...
case NOTHING_TO_DO: {
// Update our list of inferior chain tips
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getSignature());
if (!inferiorChainSignatures.contains(inferiorChainSignature))
inferiorChainSignatures.add(inferiorChainSignature);
@@ -371,8 +384,7 @@ public class Synchronizer extends Thread {
// Reset our cache of inferior chains
inferiorChainSignatures.clear();
Network network = Network.getInstance();
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
Network.getInstance().broadcastOurChain();
EventBus.INSTANCE.notify(new NewChainTipEvent(priorChainTip, newChainTip));
}
@@ -399,9 +411,10 @@ public class Synchronizer extends Thread {
timePeersLastAvailable = NTP.getTime();
// If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint
if (NTP.getTime() - timePeersLastAvailable > RECOVERY_MODE_TIMEOUT) {
long recoveryModeTimeout = Settings.getInstance().getRecoveryModeTimeout();
if (NTP.getTime() - timePeersLastAvailable > recoveryModeTimeout) {
if (recoveryMode == false) {
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", RECOVERY_MODE_TIMEOUT/60/1000));
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", recoveryModeTimeout/60/1000));
recoveryMode = true;
}
}
@@ -515,13 +528,13 @@ public class Synchronizer extends Thread {
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
final int ourInitialHeight = ourLatestBlockData.getHeight();
PeerChainTipData peerChainTipData = peer.getChainTipData();
int peerHeight = peerChainTipData.getLastHeight();
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
BlockSummaryData peerChainTipData = peer.getChainTipData();
int peerHeight = peerChainTipData.getHeight();
byte[] peersLastBlockSignature = peerChainTipData.getSignature();
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
LOGGER.debug(String.format("Fetching summaries from peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
List<BlockSummaryData> peerBlockSummaries = new ArrayList<>();
@@ -619,7 +632,7 @@ public class Synchronizer extends Thread {
// We have already determined that the correct chain diverged from a lower height. We are safe to skip these peers.
for (Peer peer : peersSharingCommonBlock) {
LOGGER.debug(String.format("Peer %s has common block at height %d but the superior chain is at height %d. Removing it from this round.", peer, commonBlockSummary.getHeight(), dropPeersAfterCommonBlockHeight));
this.addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
//this.addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
}
continue;
}
@@ -630,16 +643,18 @@ public class Synchronizer extends Thread {
int minChainLength = this.calculateMinChainLengthOfPeers(peersSharingCommonBlock, commonBlockSummary);
// Fetch block summaries from each peer
for (Peer peer : peersSharingCommonBlock) {
Iterator peersSharingCommonBlockIterator = peersSharingCommonBlock.iterator();
while (peersSharingCommonBlockIterator.hasNext()) {
Peer peer = (Peer) peersSharingCommonBlockIterator.next();
// If we're shutting down, just return the latest peer list
if (Controller.isStopping())
return peers;
// Count the number of blocks this peer has beyond our common block
final PeerChainTipData peerChainTipData = peer.getChainTipData();
final int peerHeight = peerChainTipData.getLastHeight();
final byte[] peerLastBlockSignature = peerChainTipData.getLastBlockSignature();
final BlockSummaryData peerChainTipData = peer.getChainTipData();
final int peerHeight = peerChainTipData.getHeight();
final byte[] peerLastBlockSignature = peerChainTipData.getSignature();
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
// Limit the number of blocks we are comparing. FUTURE: we could request more in batches, but there may not be a case when this is needed
int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
@@ -687,6 +702,8 @@ public class Synchronizer extends Thread {
if (this.containsInvalidBlockSummary(peer.getCommonBlockData().getBlockSummariesAfterCommonBlock())) {
LOGGER.debug("Ignoring peer %s because it holds an invalid block", peer);
peers.remove(peer);
peersSharingCommonBlockIterator.remove();
continue;
}
// Reduce minChainLength if needed. If we don't have any blocks, this peer will be excluded from chain weight comparisons later in the process, so we shouldn't update minChainLength
@@ -725,8 +742,9 @@ public class Synchronizer extends Thread {
LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
for (Peer peer : peersSharingCommonBlock) {
final int peerHeight = peer.getChainTipData().getLastHeight();
final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
BlockSummaryData peerChainTipData = peer.getChainTipData();
final int peerHeight = peerChainTipData.getHeight();
final Long peerLastBlockTimestamp = peerChainTipData.getTimestamp();
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
final CommonBlockData peerCommonBlockData = peer.getCommonBlockData();
@@ -823,7 +841,7 @@ public class Synchronizer extends Thread {
// Calculate the length of the shortest peer chain sharing this common block
int minChainLength = 0;
for (Peer peer : peersSharingCommonBlock) {
final int peerHeight = peer.getChainTipData().getLastHeight();
final int peerHeight = peer.getChainTipData().getHeight();
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
if (peerAdditionalBlocksAfterCommonBlock < minChainLength || minChainLength == 0)
@@ -842,6 +860,10 @@ public class Synchronizer extends Thread {
/* Invalid block signature tracking */
public Map<ByteArray, Long> getInvalidBlockSignatures() {
return this.invalidBlockSignatures;
}
private void addInvalidBlockSignature(byte[] signature) {
Long now = NTP.getTime();
if (now == null) {
@@ -849,8 +871,7 @@ public class Synchronizer extends Thread {
}
// Add or update existing entry
String sig58 = Base58.encode(signature);
invalidBlockSignatures.put(sig58, now);
invalidBlockSignatures.put(ByteArray.wrap(signature), now);
}
private void deleteOlderInvalidSignatures(Long now) {
if (now == null) {
@@ -869,17 +890,16 @@ public class Synchronizer extends Thread {
}
}
}
private boolean containsInvalidBlockSummary(List<BlockSummaryData> blockSummaries) {
public boolean containsInvalidBlockSummary(List<BlockSummaryData> blockSummaries) {
if (blockSummaries == null || invalidBlockSignatures == null) {
return false;
}
// Loop through our known invalid blocks and check each one against supplied block summaries
for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
byte[] invalidSignature = Base58.decode(invalidSignature58);
for (ByteArray invalidSignature : invalidBlockSignatures.keySet()) {
for (BlockSummaryData blockSummary : blockSummaries) {
byte[] signature = blockSummary.getSignature();
if (Arrays.equals(signature, invalidSignature)) {
if (Arrays.equals(signature, invalidSignature.value)) {
return true;
}
}
@@ -892,10 +912,9 @@ public class Synchronizer extends Thread {
}
// Loop through our known invalid blocks and check each one against supplied block signatures
for (String invalidSignature58 : invalidBlockSignatures.keySet()) {
byte[] invalidSignature = Base58.decode(invalidSignature58);
for (ByteArray invalidSignature : invalidBlockSignatures.keySet()) {
for (byte[] signature : blockSignatures) {
if (Arrays.equals(signature, invalidSignature)) {
if (Arrays.equals(signature, invalidSignature.value)) {
return true;
}
}
@@ -930,13 +949,13 @@ public class Synchronizer extends Thread {
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
final int ourInitialHeight = ourLatestBlockData.getHeight();
PeerChainTipData peerChainTipData = peer.getChainTipData();
int peerHeight = peerChainTipData.getLastHeight();
byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
BlockSummaryData peerChainTipData = peer.getChainTipData();
int peerHeight = peerChainTipData.getHeight();
byte[] peersLastBlockSignature = peerChainTipData.getSignature();
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
String syncString = String.format("Synchronizing with peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp());
LOGGER.info(syncString);
@@ -1243,7 +1262,14 @@ public class Synchronizer extends Thread {
int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size();
int retryCount = 0;
while (height < peerHeight) {
// Keep fetching blocks from peer until we reach their tip, or reach a count of MAXIMUM_COMMON_DELTA blocks.
// We need to limit the total number, otherwise too much can be loaded into memory, causing an
// OutOfMemoryException. This is common when syncing from 1000+ blocks behind the chain tip, after starting
// from a small fork that didn't become part of the main chain. This causes the entire sync process to
// use syncToPeerChain(), resulting in potentially thousands of blocks being held in memory if the limit
// below isn't applied.
while (height < peerHeight && peerBlocks.size() <= MAXIMUM_COMMON_DELTA) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
@@ -1310,7 +1336,7 @@ public class Synchronizer extends Thread {
// Final check to make sure the peer isn't out of date (except for when we're in recovery mode)
if (!recoveryMode && peer.getChainTipData() != null) {
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {
LOGGER.info(String.format("Peer %s is out of date, so abandoning sync attempt", peer));
return SynchronizationResult.CHAIN_TIP_TOO_OLD;
@@ -1445,6 +1471,12 @@ public class Synchronizer extends Thread {
repository.saveChanges();
synchronized (this.syncLock) {
if (peer.getChainTipData() != null) {
this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
}
}
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
@@ -1540,6 +1572,12 @@ public class Synchronizer extends Thread {
repository.saveChanges();
synchronized (this.syncLock) {
if (peer.getChainTipData() != null) {
this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
}
}
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
@@ -1550,12 +1588,19 @@ public class Synchronizer extends Thread {
Message getBlockSummariesMessage = new GetBlockSummariesMessage(parentSignature, numberRequested);
Message message = peer.getResponse(getBlockSummariesMessage);
if (message == null || message.getType() != MessageType.BLOCK_SUMMARIES)
if (message == null)
return null;
BlockSummariesMessage blockSummariesMessage = (BlockSummariesMessage) message;
if (message.getType() == MessageType.BLOCK_SUMMARIES) {
BlockSummariesMessage blockSummariesMessage = (BlockSummariesMessage) message;
return blockSummariesMessage.getBlockSummaries();
}
else if (message.getType() == MessageType.BLOCK_SUMMARIES_V2) {
BlockSummariesV2Message blockSummariesMessage = (BlockSummariesV2Message) message;
return blockSummariesMessage.getBlockSummaries();
}
return blockSummariesMessage.getBlockSummaries();
return null;
}
private List<byte[]> getBlockSignatures(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
@@ -1574,12 +1619,35 @@ public class Synchronizer extends Thread {
Message getBlockMessage = new GetBlockMessage(signature);
Message message = peer.getResponse(getBlockMessage);
if (message == null || message.getType() != MessageType.BLOCK)
if (message == null) {
peer.getPeerData().incrementFailedSyncCount();
if (peer.getPeerData().getFailedSyncCount() >= MAX_CONSECUTIVE_FAILED_SYNC_ATTEMPTS) {
// Several failed attempts, so mark peer as misbehaved
LOGGER.info("Marking peer {} as misbehaved due to {} failed sync attempts", peer, peer.getPeerData().getFailedSyncCount());
Network.getInstance().peerMisbehaved(peer);
}
return null;
}
BlockMessage blockMessage = (BlockMessage) message;
// Reset failed sync count now that we have a block response
// FUTURE: we could move this to the end of the sync process, but to reduce risk this can be done
// at a later stage. For now we are only defending against serialization errors or no responses.
peer.getPeerData().setFailedSyncCount(0);
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
switch (message.getType()) {
case BLOCK: {
BlockMessage blockMessage = (BlockMessage) message;
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
}
case BLOCK_V2: {
BlockV2Message blockMessage = (BlockV2Message) message;
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStatesHash());
}
default:
return null;
}
}
public void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {

View File

@@ -2,7 +2,9 @@ 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.data.transaction.TransactionData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.message.GetTransactionMessage;
import org.qortal.network.message.Message;
@@ -11,14 +13,15 @@ import org.qortal.network.message.TransactionSignaturesMessage;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.transform.TransformationException;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class TransactionImporter extends Thread {
@@ -55,12 +58,16 @@ public class TransactionImporter extends Thread {
@Override
public void run() {
Thread.currentThread().setName("Transaction Importer");
try {
while (!Controller.isStopping()) {
Thread.sleep(1000L);
Thread.sleep(500L);
// Process incoming transactions queue
processIncomingTransactionsQueue();
validateTransactionsInQueue();
importTransactionsInQueue();
// Clean up invalid incoming transactions list
cleanupInvalidTransactionsList(NTP.getTime());
}
@@ -87,7 +94,26 @@ public class TransactionImporter extends Thread {
incomingTransactions.keySet().removeIf(t -> Arrays.equals(t.getSignature(), signature));
}
private void processIncomingTransactionsQueue() {
/**
* Retrieve all pending unconfirmed transactions that have had their signatures validated.
* @return a list of TransactionData objects, with valid signatures.
*/
private List<TransactionData> getCachedSigValidTransactions() {
synchronized (this.incomingTransactions) {
return this.incomingTransactions.entrySet().stream()
.filter(t -> Boolean.TRUE.equals(t.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
}
/**
* Validate the signatures of any transactions pending import, then update their
* entries in the queue to mark them as valid/invalid.
*
* No database lock is required.
*/
private void validateTransactionsInQueue() {
if (this.incomingTransactions.isEmpty()) {
// Nothing to do?
return;
@@ -104,8 +130,17 @@ public class TransactionImporter extends Thread {
LOGGER.debug("Validating signatures in incoming transactions queue (size {})...", unvalidatedCount);
}
// A list of all currently pending transactions that have valid signatures
List<Transaction> sigValidTransactions = new ArrayList<>();
// A list of signatures that became valid in this round
List<byte[]> newlyValidSignatures = new ArrayList<>();
boolean isLiteNode = Settings.getInstance().isLite();
// We need the latest block in order to check for expired transactions
BlockData latestBlock = Controller.getInstance().getChainTip();
// Signature validation round - does not require blockchain lock
for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) {
// Quick exit?
@@ -115,34 +150,59 @@ public class TransactionImporter extends Thread {
TransactionData transactionData = transactionEntry.getKey();
Transaction transaction = Transaction.fromData(repository, transactionData);
String signature58 = Base58.encode(transactionData.getSignature());
Long now = NTP.getTime();
if (now == null) {
return;
}
// Drop expired transactions before they are considered "sig valid"
if (latestBlock != null && transaction.getDeadline() <= latestBlock.getTimestamp()) {
LOGGER.debug("Removing expired {} transaction {} from import queue", transactionData.getType().name(), signature58);
removeIncomingTransaction(transactionData.getSignature());
invalidUnconfirmedTransactions.put(signature58, (now + EXPIRED_TRANSACTION_RECHECK_INTERVAL));
continue;
}
// Only validate signature if we haven't already done so
Boolean isSigValid = transactionEntry.getValue();
if (!Boolean.TRUE.equals(isSigValid)) {
if (!transaction.isSignatureValid()) {
String signature58 = Base58.encode(transactionData.getSignature());
if (isLiteNode) {
// Lite nodes can't easily validate transactions, so for now we will have to assume that everything is valid
sigValidTransactions.add(transaction);
newlyValidSignatures.add(transactionData.getSignature());
// Add mark signature as valid if transaction still exists in import queue
incomingTransactions.computeIfPresent(transactionData, (k, v) -> Boolean.TRUE);
continue;
}
LOGGER.trace("Ignoring {} transaction {} with invalid signature", transactionData.getType().name(), signature58);
if (!transaction.isSignatureValid()) {
LOGGER.debug("Ignoring {} transaction {} with invalid signature", transactionData.getType().name(), signature58);
removeIncomingTransaction(transactionData.getSignature());
// Also add to invalidIncomingTransactions map
Long now = NTP.getTime();
now = NTP.getTime();
if (now != null) {
Long expiry = now + INVALID_TRANSACTION_RECHECK_INTERVAL;
LOGGER.trace("Adding stale invalid transaction {} to invalidUnconfirmedTransactions...", signature58);
LOGGER.trace("Adding invalid transaction {} to invalidUnconfirmedTransactions...", signature58);
// Add to invalidUnconfirmedTransactions so that we don't keep requesting it
invalidUnconfirmedTransactions.put(signature58, expiry);
}
// We're done with this transaction
continue;
}
else {
// Count the number that were validated in this round, for logging purposes
validatedCount++;
}
// Count the number that were validated in this round, for logging purposes
validatedCount++;
// Add mark signature as valid if transaction still exists in import queue
incomingTransactions.computeIfPresent(transactionData, (k, v) -> Boolean.TRUE);
// Signature validated in this round
newlyValidSignatures.add(transactionData.getSignature());
} else {
LOGGER.trace(() -> String.format("Transaction %s known to have valid signature", Base58.encode(transactionData.getSignature())));
}
@@ -155,30 +215,44 @@ public class TransactionImporter extends Thread {
LOGGER.debug("Finished validating signatures in incoming transactions queue (valid this round: {}, total pending import: {})...", validatedCount, sigValidTransactions.size());
}
if (sigValidTransactions.isEmpty()) {
// Don't bother locking if there are no new transactions to process
return;
if (!newlyValidSignatures.isEmpty()) {
LOGGER.debug("Broadcasting {} newly valid signatures ahead of import", newlyValidSignatures.size());
Message newTransactionSignatureMessage = new TransactionSignaturesMessage(newlyValidSignatures);
Network.getInstance().broadcast(broadcastPeer -> newTransactionSignatureMessage);
}
if (Synchronizer.getInstance().isSyncRequested() || Synchronizer.getInstance().isSynchronizing()) {
// Prioritize syncing, and don't attempt to lock
// Signature validity is retained in the incomingTransactions map, to avoid the above work being wasted
return;
}
} catch (DataException e) {
LOGGER.error("Repository issue while processing incoming transactions", e);
}
}
try {
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock(2, TimeUnit.SECONDS)) {
// Signature validity is retained in the incomingTransactions map, to avoid the above work being wasted
LOGGER.debug("Too busy to process incoming transactions queue");
return;
}
} catch (InterruptedException e) {
LOGGER.debug("Interrupted when trying to acquire blockchain lock");
return;
}
/**
* Import any transactions in the queue that have valid signatures.
*
* A database lock is required.
*/
private void importTransactionsInQueue() {
List<TransactionData> sigValidTransactions = this.getCachedSigValidTransactions();
if (sigValidTransactions.isEmpty()) {
// Don't bother locking if there are no new transactions to process
return;
}
LOGGER.debug("Processing incoming transactions queue (size {})...", sigValidTransactions.size());
if (Synchronizer.getInstance().isSyncRequested() || Synchronizer.getInstance().isSynchronizing()) {
// Prioritize syncing, and don't attempt to lock
return;
}
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock()) {
LOGGER.debug("Too busy to import incoming transactions queue");
return;
}
LOGGER.debug("Importing incoming transactions queue (size {})...", sigValidTransactions.size());
int processedCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
// Import transactions with valid signatures
try {
@@ -188,14 +262,15 @@ public class TransactionImporter extends Thread {
}
if (Synchronizer.getInstance().isSyncRequestPending()) {
LOGGER.debug("Breaking out of transaction processing with {} remaining, because a sync request is pending", sigValidTransactions.size() - i);
LOGGER.debug("Breaking out of transaction importing with {} remaining, because a sync request is pending", sigValidTransactions.size() - i);
return;
}
Transaction transaction = sigValidTransactions.get(i);
TransactionData transactionData = transaction.getTransactionData();
TransactionData transactionData = sigValidTransactions.get(i);
Transaction transaction = Transaction.fromData(repository, transactionData);
Transaction.ValidationResult validationResult = transaction.importAsUnconfirmed();
processedCount++;
switch (validationResult) {
case TRANSACTION_ALREADY_EXISTS: {
@@ -217,7 +292,7 @@ public class TransactionImporter extends Thread {
// All other invalid cases:
default: {
final String signature58 = Base58.encode(transactionData.getSignature());
LOGGER.trace(() -> String.format("Ignoring invalid (%s) %s transaction %s", validationResult.name(), transactionData.getType().name(), signature58));
LOGGER.debug(() -> String.format("Ignoring invalid (%s) %s transaction %s", validationResult.name(), transactionData.getType().name(), signature58));
Long now = NTP.getTime();
if (now != null && now - transactionData.getTimestamp() > INVALID_TRANSACTION_STALE_TIMEOUT) {
@@ -240,12 +315,11 @@ public class TransactionImporter extends Thread {
removeIncomingTransaction(transactionData.getSignature());
}
} finally {
LOGGER.debug("Finished processing incoming transactions queue");
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
LOGGER.debug("Finished importing {} incoming transaction{}", processedCount, (processedCount == 1 ? "" : "s"));
blockchainLock.unlock();
}
} catch (DataException e) {
LOGGER.error("Repository issue while processing incoming transactions", e);
LOGGER.error("Repository issue while importing incoming transactions", e);
}
}
@@ -278,8 +352,18 @@ public class TransactionImporter extends Thread {
byte[] signature = getTransactionMessage.getSignature();
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
// Firstly check the sig-valid transactions that are currently queued for import
TransactionData transactionData = this.getCachedSigValidTransactions().stream()
.filter(t -> Arrays.equals(signature, t.getSignature()))
.findFirst().orElse(null);
if (transactionData == null) {
// Not found in import queue, so try the database
transactionData = repository.getTransactionRepository().fromSignature(signature);
}
if (transactionData == null) {
// Still not found - so we don't have this transaction
LOGGER.debug(() -> String.format("Ignoring GET_TRANSACTION request from peer %s for unknown transaction %s", peer, Base58.encode(signature)));
// Send no response at all???
return;

View File

@@ -67,6 +67,9 @@ public class ArbitraryDataFileListManager {
/** Maximum number of hops that a file list relay request is allowed to make */
public static int RELAY_REQUEST_MAX_HOPS = 4;
/** Minimum peer version to use relay */
public static String RELAY_MIN_PEER_VERSION = "3.4.0";
private ArbitraryDataFileListManager() {
}
@@ -120,12 +123,22 @@ public class ArbitraryDataFileListManager {
}
}
// Then allow another 5 attempts, each 5 minutes apart
// Then allow another 3 attempts, each 5 minutes apart
if (timeSinceLastAttempt > 5 * 60 * 1000L) {
// We haven't tried for at least 5 minutes
if (networkBroadcastCount < 5) {
// We've made less than 5 total attempts
if (networkBroadcastCount < 6) {
// We've made less than 6 total attempts
return true;
}
}
// Then allow another 4 attempts, each 30 minutes apart
if (timeSinceLastAttempt > 30 * 60 * 1000L) {
// We haven't tried for at least 5 minutes
if (networkBroadcastCount < 10) {
// We've made less than 10 total attempts
return true;
}
}
@@ -184,8 +197,8 @@ public class ArbitraryDataFileListManager {
}
}
if (timeSinceLastAttempt > 24 * 60 * 60 * 1000L) {
// We haven't tried for at least 24 hours
if (timeSinceLastAttempt > 60 * 60 * 1000L) {
// We haven't tried for at least 1 hour
return true;
}
@@ -283,8 +296,8 @@ public class ArbitraryDataFileListManager {
LOGGER.debug(String.format("Sending data file list request for signature %s with %d hashes to %d peers...", signature58, hashCount, handshakedPeers.size()));
// FUTURE: send our address as requestingPeer once enough peers have switched to the new protocol
String requestingPeer = null; // Network.getInstance().getOurExternalIpAddressAndPort();
// Send our address as requestingPeer, to allow for potential direct connections with seeds/peers
String requestingPeer = Network.getInstance().getOurExternalIpAddressAndPort();
// Build request
Message getArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, missingHashes, now, 0, requestingPeer);
@@ -524,6 +537,7 @@ public class ArbitraryDataFileListManager {
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
}
forwardArbitraryDataFileListMessage.setId(message.getId());
// Forward to requesting peer
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
@@ -636,6 +650,9 @@ public class ArbitraryDataFileListManager {
// We should only respond if we have at least one hash
if (hashes.size() > 0) {
// Firstly we should keep track of the requesting peer, to allow for potential direct connections later
ArbitraryDataFileManager.getInstance().addRecentDataRequest(requestingPeer);
// We have all the chunks, so update requests map to reflect that we've sent it
// There is no need to keep track of the request, as we can serve all the chunks
if (allChunksExist) {
@@ -687,12 +704,14 @@ public class ArbitraryDataFileListManager {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
Message relayGetArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops, requestingPeer);
relayGetArbitraryDataFileListMessage.setId(message.getId());
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
Network.getInstance().broadcast(
broadcastPeer -> broadcastPeer == peer ||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
? null : relayGetArbitraryDataFileListMessage);
broadcastPeer ->
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryDataFileListMessage
);
}
else {

View File

@@ -1,5 +1,6 @@
package org.qortal.controller.arbitrary;
import com.google.common.net.InetAddresses;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.arbitrary.ArbitraryDataFile;
@@ -54,6 +55,13 @@ public class ArbitraryDataFileManager extends Thread {
*/
private List<ArbitraryDirectConnectionInfo> directConnectionInfo = Collections.synchronizedList(new ArrayList<>());
/**
* Map to keep track of peers requesting QDN data that we hold.
* Key = peer address string, value = time of last request.
* This allows for additional "burst" connections beyond existing limits.
*/
private Map<String, Long> recentDataRequests = Collections.synchronizedMap(new HashMap<>());
public static int MAX_FILE_HASH_RESPONSES = 1000;
@@ -108,6 +116,9 @@ public class ArbitraryDataFileManager extends Thread {
final long directConnectionInfoMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT;
directConnectionInfo.removeIf(entry -> entry.getTimestamp() < directConnectionInfoMinimumTimestamp);
final long recentDataRequestMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RECENT_DATA_REQUESTS_TIMEOUT;
recentDataRequests.entrySet().removeIf(entry -> entry.getValue() < recentDataRequestMinimumTimestamp);
}
@@ -490,6 +501,45 @@ public class ArbitraryDataFileManager extends Thread {
}
// Peers requesting QDN data from us
/**
* Add an address string of a peer that is trying to request data from us.
* @param peerAddress
*/
public void addRecentDataRequest(String peerAddress) {
if (peerAddress == null) {
return;
}
Long now = NTP.getTime();
if (now == null) {
return;
}
// Make sure to remove the port, since it isn't guaranteed to match next time
String[] parts = peerAddress.split(":");
if (parts.length == 0) {
return;
}
String host = parts[0];
if (!InetAddresses.isInetAddress(host)) {
// Invalid host
return;
}
this.recentDataRequests.put(host, now);
}
public boolean isPeerRequestingData(String peerAddressWithoutPort) {
return this.recentDataRequests.containsKey(peerAddressWithoutPort);
}
public boolean hasPendingDataRequest() {
return !this.recentDataRequests.isEmpty();
}
// Network handlers
public void onNetworkGetArbitraryDataFileMessage(Peer peer, Message message) {
@@ -545,9 +595,10 @@ public class ArbitraryDataFileManager extends Thread {
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
LOGGER.debug(String.format("Sending 'file unknown' response to peer %s for GET_FILE request for unknown file %s", peer, arbitraryDataFile));
// We'll send empty block summaries message as it's very short
// TODO: use a different message type here
Message fileUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
// Send generic 'unknown' message as it's very short
Message fileUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
? new GenericUnknownMessage()
: new BlockSummariesMessage(Collections.emptyList());
fileUnknownMessage.setId(message.getId());
if (!peer.sendMessage(fileUnknownMessage)) {
LOGGER.debug("Couldn't sent file-unknown response");

View File

@@ -47,6 +47,9 @@ public class ArbitraryDataManager extends Thread {
/** Maximum time to hold direct peer connection information */
public static final long ARBITRARY_DIRECT_CONNECTION_INFO_TIMEOUT = 2 * 60 * 1000L; // ms
/** Maximum time to hold information about recent data requests that we can fulfil */
public static final long ARBITRARY_RECENT_DATA_REQUESTS_TIMEOUT = 2 * 60 * 1000L; // ms
/** Maximum number of hops that an arbitrary signatures request is allowed to make */
private static int ARBITRARY_SIGNATURES_REQUEST_MAX_HOPS = 3;

View File

@@ -22,8 +22,7 @@ import org.qortal.utils.Triple;
import java.io.IOException;
import java.util.*;
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.RELAY_REQUEST_MAX_DURATION;
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.RELAY_REQUEST_MAX_HOPS;
import static org.qortal.controller.arbitrary.ArbitraryDataFileListManager.*;
public class ArbitraryMetadataManager {
@@ -339,6 +338,7 @@ public class ArbitraryMetadataManager {
if (requestingPeer != null) {
ArbitraryMetadataMessage forwardArbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, arbitraryMetadataMessage.getArbitraryMetadataFile());
forwardArbitraryMetadataMessage.setId(arbitraryMetadataMessage.getId());
// Forward to requesting peer
LOGGER.debug("Forwarding metadata to requesting peer: {}", requestingPeer);
@@ -434,12 +434,13 @@ public class ArbitraryMetadataManager {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
relayGetArbitraryMetadataMessage.setId(message.getId());
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
Network.getInstance().broadcast(
broadcastPeer -> broadcastPeer == peer ||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
? null : relayGetArbitraryMetadataMessage);
broadcastPeer ->
!broadcastPeer.isAtLeastVersion(RELAY_MIN_PEER_VERSION) ? null :
broadcastPeer == peer || Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost()) ? null : relayGetArbitraryMetadataMessage);
}
else {

View File

@@ -19,6 +19,11 @@ public class AtStatesPruner implements Runnable {
public void run() {
Thread.currentThread().setName("AT States pruner");
if (Settings.getInstance().isLite()) {
// Nothing to prune in lite mode
return;
}
boolean archiveMode = false;
if (!Settings.getInstance().isTopOnly()) {
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving

View File

@@ -19,6 +19,11 @@ public class AtStatesTrimmer implements Runnable {
public void run() {
Thread.currentThread().setName("AT States trimmer");
if (Settings.getInstance().isLite()) {
// Nothing to trim in lite mode
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
int trimStartHeight = repository.getATRepository().getAtTrimHeight();

View File

@@ -16,12 +16,12 @@ public class BlockArchiver implements Runnable {
private static final Logger LOGGER = LogManager.getLogger(BlockArchiver.class);
private static final long INITIAL_SLEEP_PERIOD = 0L; // TODO: 5 * 60 * 1000L + 1234L; // ms
private static final long INITIAL_SLEEP_PERIOD = 5 * 60 * 1000L + 1234L; // ms
public void run() {
Thread.currentThread().setName("Block archiver");
if (!Settings.getInstance().isArchiveEnabled()) {
if (!Settings.getInstance().isArchiveEnabled() || Settings.getInstance().isLite()) {
return;
}

View File

@@ -19,6 +19,11 @@ public class BlockPruner implements Runnable {
public void run() {
Thread.currentThread().setName("Block pruner");
if (Settings.getInstance().isLite()) {
// Nothing to prune in lite mode
return;
}
boolean archiveMode = false;
if (!Settings.getInstance().isTopOnly()) {
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving

View File

@@ -107,7 +107,7 @@ public class NamesDatabaseIntegrityCheck {
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
Name nameObj = new Name(repository, buyNameTransactionData.getName());
if (nameObj != null && nameObj.getNameData() != null) {
nameObj.buy(buyNameTransactionData);
nameObj.buy(buyNameTransactionData, false);
modificationCount++;
LOGGER.trace("Processed BUY_NAME transaction for name {}", name);
}

View File

@@ -21,6 +21,11 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
public void run() {
Thread.currentThread().setName("Online Accounts trimmer");
if (Settings.getInstance().isLite()) {
// Nothing to trim in lite mode
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
// Don't even start trimming until initial rush has ended
Thread.sleep(INITIAL_SLEEP_PERIOD);

View File

@@ -45,9 +45,9 @@ import static java.util.stream.Collectors.toMap;
* <li>Trade-bot entries</li>
* </ul>
*/
public class LitecoinACCTv2TradeBot implements AcctTradeBot {
public class BitcoinACCTv3TradeBot implements AcctTradeBot {
private static final Logger LOGGER = LogManager.getLogger(LitecoinACCTv2TradeBot.class);
private static final Logger LOGGER = LogManager.getLogger(BitcoinACCTv3TradeBot.class);
public enum State implements TradeBot.StateNameAndValueSupplier {
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
@@ -91,18 +91,18 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
private static LitecoinACCTv2TradeBot instance;
private static BitcoinACCTv3TradeBot instance;
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
.map(State::name)
.collect(Collectors.toUnmodifiableList());
private LitecoinACCTv2TradeBot() {
private BitcoinACCTv3TradeBot() {
}
public static synchronized LitecoinACCTv2TradeBot getInstance() {
public static synchronized BitcoinACCTv3TradeBot getInstance() {
if (instance == null)
instance = new LitecoinACCTv2TradeBot();
instance = new BitcoinACCTv3TradeBot();
return instance;
}
@@ -113,7 +113,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
}
/**
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for LTC.
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for BTC.
* <p>
* Generates:
* <ul>
@@ -122,14 +122,14 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
* Derives:
* <ul>
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
* <li>'foreign' (as in Litecoin) public key, public key hash</li>
* <li>'foreign' (as in Bitcoin) public key, public key hash</li>
* </ul>
* A Qortal AT is then constructed including the following as constants in the 'data segment':
* <ul>
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
* <li>'foreign'/Litecoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
* <li>'foreign'/Bitcoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
* <li>QORT amount on offer by Bob</li>
* <li>LTC amount expected in return by Bob (from Alice)</li>
* <li>BTC amount expected in return by Bob (from Alice)</li>
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
* </ul>
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
@@ -151,17 +151,17 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
// Convert Litecoin receiving address into public key hash (we only support P2PKH at this time)
Address litecoinReceivingAddress;
// Convert Bitcoin receiving address into public key hash (we only support P2PKH at this time)
Address bitcoinReceivingAddress;
try {
litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
bitcoinReceivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
} catch (AddressFormatException e) {
throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress);
}
if (litecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
if (bitcoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress);
byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.getHash();
byte[] bitcoinReceivingAccountInfo = bitcoinReceivingAddress.getHash();
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
@@ -172,11 +172,11 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
byte[] signature = null;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
String name = "QORT/LTC ACCT";
String description = "QORT/LTC cross-chain trade";
String name = "QORT/BTC ACCT";
String description = "QORT/BTC cross-chain trade";
String aTType = "ACCT";
String tags = "ACCT QORT LTC";
byte[] creationBytes = LitecoinACCTv2.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
String tags = "ACCT QORT BTC";
byte[] creationBytes = BitcoinACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
long amount = tradeBotCreateRequest.fundingQortAmount;
@@ -189,14 +189,14 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
String atAddress = deployAtTransactionData.getAtAddress();
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, LitecoinACCTv2.NAME,
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, BitcoinACCTv3.NAME,
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
null, null,
SupportedBlockchain.LITECOIN.name(),
SupportedBlockchain.BITCOIN.name(),
tradeForeignPublicKey, tradeForeignPublicKeyHash,
tradeBotCreateRequest.foreignAmount, null, null, null, litecoinReceivingAccountInfo);
tradeBotCreateRequest.foreignAmount, null, null, null, bitcoinReceivingAccountInfo);
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
@@ -212,15 +212,15 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
}
/**
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching LTC to an existing offer.
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching BTC to an existing offer.
* <p>
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
* and access to a Litecoin wallet via <tt>xprv58</tt>.
* and access to a Bitcoin wallet via <tt>xprv58</tt>.
* <p>
* The <tt>crossChainTradeData</tt> contains the current trade offer state
* as extracted from the AT's data segment.
* <p>
* Access to a funded wallet is via a Litecoin BIP32 hierarchical deterministic key,
* Access to a funded wallet is via a Bitcoin BIP32 hierarchical deterministic key,
* passed via <tt>xprv58</tt>.
* <b>This key will be stored in your node's database</b>
* to allow trade-bot to create/fund the necessary P2SH transactions!
@@ -230,15 +230,15 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
* Electrum wallet by going to the console tab and entering:<br>
* <tt>wallet.keystore.xprv</tt><br>
* which should result in a base58 string starting with either 'xprv' (for Litecoin main-net)
* or 'tprv' for (Litecoin test-net).
* which should result in a base58 string starting with either 'xprv' (for Bitcoin main-net)
* or 'tprv' for (Bitcoin test-net).
* <p>
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
* <p>
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
* with the Litecoin amount expected by 'Bob'.
* with the Bitcoin amount expected by 'Bob'.
* <p>
* If the Litecoin transaction is successfully broadcast to the network then
* If the Bitcoin transaction is successfully broadcast to the network then
* we also send a MESSAGE to Bob's trade-bot to let them know.
* <p>
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
@@ -246,7 +246,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
* @param repository
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
* @param xprv58 funded wallet xprv in base58
* @return true if P2SH-A funding transaction successfully broadcast to Litecoin network, false otherwise
* @return true if P2SH-A funding transaction successfully broadcast to Bitcoin network, false otherwise
* @throws DataException
*/
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
@@ -266,12 +266,12 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
long now = NTP.getTime();
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, LitecoinACCTv2.NAME,
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, BitcoinACCTv3.NAME,
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secretA, hashOfSecretA,
SupportedBlockchain.LITECOIN.name(),
SupportedBlockchain.BITCOIN.name(),
tradeForeignPublicKey, tradeForeignPublicKeyHash,
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
@@ -282,9 +282,9 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
long p2shFee;
try {
p2shFee = Litecoin.getInstance().getP2shFee(now);
p2shFee = Bitcoin.getInstance().getP2shFee(now);
} catch (ForeignBlockchainException e) {
LOGGER.debug("Couldn't estimate Litecoin fees?");
LOGGER.debug("Couldn't estimate Bitcoin fees?");
return ResponseResult.NETWORK_ISSUE;
}
@@ -294,17 +294,17 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
// P2SH-A to be funded
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
String p2shAddress = Litecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
String p2shAddress = Bitcoin.getInstance().deriveP2shAddress(redeemScriptBytes);
// Build transaction for funding P2SH-A
Transaction p2shFundingTransaction = Litecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
Transaction p2shFundingTransaction = Bitcoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
if (p2shFundingTransaction == null) {
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
return ResponseResult.BALANCE_ISSUE;
}
try {
Litecoin.getInstance().broadcastTransaction(p2shFundingTransaction);
Bitcoin.getInstance().broadcastTransaction(p2shFundingTransaction);
} catch (ForeignBlockchainException e) {
// We couldn't fund P2SH-A at this time
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
@@ -312,7 +312,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
}
// Attempt to send MESSAGE to Bob's Qortal trade address
byte[] messageData = LitecoinACCTv2.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
byte[] messageData = BitcoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
@@ -382,7 +382,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
}
if (tradeBotState.requiresTradeData) {
tradeData = LitecoinACCTv2.getInstance().populateTradeData(repository, atData);
tradeData = BitcoinACCTv3.getInstance().populateTradeData(repository, atData);
if (tradeData == null) {
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
return;
@@ -463,7 +463,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
* <p>
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
* <p>
* Assuming P2SH-A has at least expected Litecoin balance,
* Assuming P2SH-A has at least expected Bitcoin balance,
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
* <p>
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
@@ -481,7 +481,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
return;
}
Litecoin litecoin = Litecoin.getInstance();
Bitcoin bitcoin = Bitcoin.getInstance();
String address = tradeBotData.getTradeNativeAddress();
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
@@ -490,27 +490,27 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
if (messageTransactionData.isText())
continue;
// We're expecting: HASH160(secret-A), Alice's Litecoin pubkeyhash and lockTime-A
// We're expecting: HASH160(secret-A), Alice's Bitcoin pubkeyhash and lockTime-A
byte[] messageData = messageTransactionData.getData();
LitecoinACCTv2.OfferMessageData offerMessageData = LitecoinACCTv2.extractOfferMessageData(messageData);
BitcoinACCTv3.OfferMessageData offerMessageData = BitcoinACCTv3.extractOfferMessageData(messageData);
if (offerMessageData == null)
continue;
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerLitecoinPKH;
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA;
long messageTimestamp = messageTransactionData.getTimestamp();
int refundTimeout = LitecoinACCTv2.calcRefundTimeout(messageTimestamp, lockTimeA);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
// Determine P2SH-A address and confirm funded
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
@@ -540,7 +540,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
byte[] outgoingMessageData = LitecoinACCTv2.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
byte[] outgoingMessageData = BitcoinACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
@@ -579,7 +579,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
* <p>
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
* <p>
* In revealing a valid secret-A, Bob can then redeem the LTC funds from P2SH-A.
* In revealing a valid secret-A, Bob can then redeem the BTC funds from P2SH-A.
* <p>
* @throws ForeignBlockchainException
*/
@@ -588,19 +588,19 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
return;
Litecoin litecoin = Litecoin.getInstance();
Bitcoin bitcoin = Bitcoin.getInstance();
int lockTimeA = tradeBotData.getLockTimeA();
// Refund P2SH-A if we've passed lockTime-A
if (NTP.getTime() >= lockTimeA * 1000L) {
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
@@ -646,7 +646,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
}
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
int refundTimeout = LitecoinACCTv2.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
int refundTimeout = BitcoinACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
// Our calculated refundTimeout should match AT's refundTimeout
if (refundTimeout != crossChainTradeData.refundTimeout) {
@@ -660,7 +660,7 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
// Send 'redeem' MESSAGE to AT using both secret
byte[] secretA = tradeBotData.getSecret();
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
byte[] messageData = LitecoinACCTv2.buildRedeemMessage(secretA, qortalReceivingAddress);
byte[] messageData = BitcoinACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
@@ -687,15 +687,15 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
}
/**
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the LTC funds from P2SH-A.
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the BTC funds from P2SH-A.
* <p>
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
* trade-bot is done with this specific trade and finalizes in refunded state.
* <p>
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the LTC funds from P2SH-A
* to Bob's 'foreign'/Litecoin trade legacy-format address, as derived from trade private key.
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the BTC funds from P2SH-A
* to Bob's 'foreign'/Bitcoin trade legacy-format address, as derived from trade private key.
* <p>
* (This could potentially be 'improved' to send LTC to any address of Bob's choosing by changing the transaction output).
* (This could potentially be 'improved' to send BTC to any address of Bob's choosing by changing the transaction output).
* <p>
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
* @throws ForeignBlockchainException
@@ -709,14 +709,14 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
// If AT is REFUNDED or CANCELLED then something has gone wrong
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the LTC
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the BTC
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
return;
}
byte[] secretA = LitecoinACCTv2.getInstance().findSecretA(repository, crossChainTradeData);
byte[] secretA = BitcoinACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
if (secretA == null) {
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
return;
@@ -724,18 +724,18 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
// Use secret-A to redeem P2SH-A
Litecoin litecoin = Litecoin.getInstance();
Bitcoin bitcoin = Bitcoin.getInstance();
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
int lockTimeA = crossChainTradeData.lockTimeA;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
@@ -756,17 +756,17 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
case FUNDED: {
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
litecoin.broadcastTransaction(p2shRedeemTransaction);
bitcoin.broadcastTransaction(p2shRedeemTransaction);
break;
}
}
String receivingAddress = litecoin.pkhToAddress(receivingAccountInfo);
String receivingAddress = bitcoin.pkhToAddress(receivingAccountInfo);
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
@@ -784,21 +784,21 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
if (NTP.getTime() <= lockTimeA * 1000L)
return;
Litecoin litecoin = Litecoin.getInstance();
Bitcoin bitcoin = Bitcoin.getInstance();
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
int medianBlockTime = litecoin.getMedianBlockTime();
int medianBlockTime = bitcoin.getMedianBlockTime();
if (medianBlockTime <= lockTimeA)
return;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
long p2shFee = Bitcoin.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
@@ -820,16 +820,16 @@ public class LitecoinACCTv2TradeBot implements AcctTradeBot {
case FUNDED:{
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
// Determine receive address for refund
String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
Address receiving = Address.fromString(bitcoin.getNetworkParameters(), receiveAddress);
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey,
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
litecoin.broadcastTransaction(p2shRefundTransaction);
bitcoin.broadcastTransaction(p2shRefundTransaction);
break;
}
}

View File

@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,27 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
// Do this in a new thread so caller doesn't have to wait for computeNonce()
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
new Thread(() -> {
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
// reset repository state to prevent deadlock
threadsRepository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
}
} catch (DataException e) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
}
}, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));

View File

@@ -1,9 +1,10 @@
package org.qortal.controller.tradebot;
import com.google.common.hash.HashCode;
import com.rust.litewalletjni.LiteWalletJni;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script.ScriptType;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
@@ -45,9 +46,9 @@ import static java.util.stream.Collectors.toMap;
* <li>Trade-bot entries</li>
* </ul>
*/
public class DogecoinACCTv2TradeBot implements AcctTradeBot {
public class PirateChainACCTv3TradeBot implements AcctTradeBot {
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2TradeBot.class);
private static final Logger LOGGER = LogManager.getLogger(PirateChainACCTv3TradeBot.class);
public enum State implements TradeBot.StateNameAndValueSupplier {
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
@@ -91,18 +92,18 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
private static DogecoinACCTv2TradeBot instance;
private static PirateChainACCTv3TradeBot instance;
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
.map(State::name)
.collect(Collectors.toUnmodifiableList());
private DogecoinACCTv2TradeBot() {
private PirateChainACCTv3TradeBot() {
}
public static synchronized DogecoinACCTv2TradeBot getInstance() {
public static synchronized PirateChainACCTv3TradeBot getInstance() {
if (instance == null)
instance = new DogecoinACCTv2TradeBot();
instance = new PirateChainACCTv3TradeBot();
return instance;
}
@@ -113,7 +114,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
}
/**
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for DOGE.
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for ARRR.
* <p>
* Generates:
* <ul>
@@ -122,14 +123,14 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
* Derives:
* <ul>
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
* <li>'foreign' (as in Dogecoin) public key, public key hash</li>
* <li>'foreign' (as in PirateChain) public key, public key hash</li>
* </ul>
* A Qortal AT is then constructed including the following as constants in the 'data segment':
* <ul>
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
* <li>'foreign'/Dogecoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
* <li>'foreign'/PirateChain public key hash - used by Alice's P2SH scripts to allow redeem</li>
* <li>QORT amount on offer by Bob</li>
* <li>DOGE amount expected in return by Bob (from Alice)</li>
* <li>ARRR amount expected in return by Bob (from Alice)</li>
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
* </ul>
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
@@ -151,17 +152,18 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
// Convert Dogecoin receiving address into public key hash (we only support P2PKH at this time)
Address dogecoinReceivingAddress;
try {
dogecoinReceivingAddress = Address.fromString(Dogecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
} catch (AddressFormatException e) {
throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
// ARRR wallet must be loaded before a trade can be created
// This is to stop trades from nodes on unsupported architectures (e.g. 32bit)
if (!LiteWalletJni.isLoaded()) {
throw new DataException("Pirate wallet not found. Check wallets screen for details.");
}
if (dogecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
byte[] dogecoinReceivingAccountInfo = dogecoinReceivingAddress.getHash();
if (!PirateChain.getInstance().isValidAddress(tradeBotCreateRequest.receivingAddress)) {
throw new DataException("Unsupported Pirate Chain receiving address: " + tradeBotCreateRequest.receivingAddress);
}
Bech32.Bech32Data decodedReceivingAddress = Bech32.decode(tradeBotCreateRequest.receivingAddress);
byte[] pirateChainReceivingAccountInfo = decodedReceivingAddress.data;
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
@@ -172,11 +174,11 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
byte[] signature = null;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
String name = "QORT/DOGE ACCT";
String description = "QORT/DOGE cross-chain trade";
String name = "QORT/ARRR ACCT";
String description = "QORT/ARRR cross-chain trade";
String aTType = "ACCT";
String tags = "ACCT QORT DOGE";
byte[] creationBytes = DogecoinACCTv2.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
String tags = "ACCT QORT ARRR";
byte[] creationBytes = PirateChainACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKey, tradeBotCreateRequest.qortAmount,
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
long amount = tradeBotCreateRequest.fundingQortAmount;
@@ -189,14 +191,14 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
String atAddress = deployAtTransactionData.getAtAddress();
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv2.NAME,
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, PirateChainACCTv3.NAME,
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
null, null,
SupportedBlockchain.DOGECOIN.name(),
SupportedBlockchain.PIRATECHAIN.name(),
tradeForeignPublicKey, tradeForeignPublicKeyHash,
tradeBotCreateRequest.foreignAmount, null, null, null, dogecoinReceivingAccountInfo);
tradeBotCreateRequest.foreignAmount, null, null, null, pirateChainReceivingAccountInfo);
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
@@ -212,15 +214,15 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
}
/**
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching DOGE to an existing offer.
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching ARRR to an existing offer.
* <p>
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
* and access to a Dogecoin wallet via <tt>xprv58</tt>.
* and access to a PirateChain wallet via <tt>xprv58</tt>.
* <p>
* The <tt>crossChainTradeData</tt> contains the current trade offer state
* as extracted from the AT's data segment.
* <p>
* Access to a funded wallet is via a Dogecoin BIP32 hierarchical deterministic key,
* Access to a funded wallet is via a PirateChain BIP32 hierarchical deterministic key,
* passed via <tt>xprv58</tt>.
* <b>This key will be stored in your node's database</b>
* to allow trade-bot to create/fund the necessary P2SH transactions!
@@ -230,26 +232,26 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
* Electrum wallet by going to the console tab and entering:<br>
* <tt>wallet.keystore.xprv</tt><br>
* which should result in a base58 string starting with either 'xprv' (for Dogecoin main-net)
* or 'tprv' for (Dogecoin test-net).
* which should result in a base58 string starting with either 'xprv' (for PirateChain main-net)
* or 'tprv' for (PirateChain test-net).
* <p>
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
* <p>
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
* with the Dogecoin amount expected by 'Bob'.
* with the PirateChain amount expected by 'Bob'.
* <p>
* If the Dogecoin transaction is successfully broadcast to the network then
* If the PirateChain transaction is successfully broadcast to the network then
* we also send a MESSAGE to Bob's trade-bot to let them know.
* <p>
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
* <p>
* @param repository
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
* @param xprv58 funded wallet xprv in base58
* @return true if P2SH-A funding transaction successfully broadcast to Dogecoin network, false otherwise
* @param seed58 funded wallet xprv in base58
* @return true if P2SH-A funding transaction successfully broadcast to PirateChain network, false otherwise
* @throws DataException
*/
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String seed58, String receivingAddress) throws DataException {
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
byte[] secretA = TradeBot.generateSecret();
byte[] hashOfSecretA = Crypto.hash160(secretA);
@@ -262,18 +264,22 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
String tradePrivateKey58 = Base58.encode(tradePrivateKey);
String tradeForeignPublicKey58 = Base58.encode(tradeForeignPublicKey);
String secret58 = Base58.encode(secretA);
// We need to generate lockTime-A: add tradeTimeout to now
long now = NTP.getTime();
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv2.NAME,
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, PirateChainACCTv3.NAME,
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secretA, hashOfSecretA,
SupportedBlockchain.DOGECOIN.name(),
SupportedBlockchain.PIRATECHAIN.name(),
tradeForeignPublicKey, tradeForeignPublicKeyHash,
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
crossChainTradeData.expectedForeignAmount, seed58, null, lockTimeA, receivingPublicKeyHash);
// Attempt to backup the trade bot data
// Include tradeBotData as an additional parameter, since it's not in the repository yet
@@ -282,9 +288,9 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
long p2shFee;
try {
p2shFee = Dogecoin.getInstance().getP2shFee(now);
p2shFee = PirateChain.getInstance().getP2shFee(now);
} catch (ForeignBlockchainException e) {
LOGGER.debug("Couldn't estimate Dogecoin fees?");
LOGGER.debug("Couldn't estimate PirateChain fees?");
return ResponseResult.NETWORK_ISSUE;
}
@@ -293,26 +299,23 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
// P2SH-A to be funded
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
String p2shAddress = Dogecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
byte[] redeemScriptBytes = PirateChainHTLC.buildScript(tradeForeignPublicKey, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
String p2shAddressT3 = PirateChain.getInstance().deriveP2shAddress(redeemScriptBytes); // Use t3 prefix when funding
byte[] redeemScriptWithPrefixBytes = PirateChainHTLC.buildScriptWithPrefix(tradeForeignPublicKey, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
String redeemScriptWithPrefix58 = Base58.encode(redeemScriptWithPrefixBytes);
// Build transaction for funding P2SH-A
Transaction p2shFundingTransaction = Dogecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
if (p2shFundingTransaction == null) {
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
// Send to P2SH address
try {
String txid = PirateChain.getInstance().fundP2SH(seed58, p2shAddressT3, amountA, redeemScriptWithPrefix58);
LOGGER.info("fundingTxidHex: {}", txid);
} catch (ForeignBlockchainException e) {
LOGGER.debug("Unable to build and send P2SH-A funding transaction - lack of funds?");
return ResponseResult.BALANCE_ISSUE;
}
try {
Dogecoin.getInstance().broadcastTransaction(p2shFundingTransaction);
} catch (ForeignBlockchainException e) {
// We couldn't fund P2SH-A at this time
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
return ResponseResult.NETWORK_ISSUE;
}
// Attempt to send MESSAGE to Bob's Qortal trade address
byte[] messageData = DogecoinACCTv2.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
byte[] messageData = PirateChainACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKey(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
@@ -333,11 +336,21 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddressT3));
return ResponseResult.OK;
}
public static String hex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte aByte : bytes) {
result.append(String.format("%02x", aByte));
// upper case
// result.append(String.format("%02X", aByte));
}
return result.toString();
}
@Override
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
@@ -354,6 +367,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
case BOB_DONE:
case ALICE_REFUNDED:
case BOB_REFUNDED:
case ALICE_REFUNDING_A:
return true;
default:
@@ -381,7 +395,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
}
if (tradeBotState.requiresTradeData) {
tradeData = DogecoinACCTv2.getInstance().populateTradeData(repository, atData);
tradeData = PirateChainACCTv3.getInstance().populateTradeData(repository, atData);
if (tradeData == null) {
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
return;
@@ -462,7 +476,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
* <p>
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
* <p>
* Assuming P2SH-A has at least expected Dogecoin balance,
* Assuming P2SH-A has at least expected PirateChain balance,
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
* <p>
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
@@ -480,7 +494,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
return;
}
Dogecoin dogecoin = Dogecoin.getInstance();
PirateChain pirateChain = PirateChain.getInstance();
String address = tradeBotData.getTradeNativeAddress();
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
@@ -489,27 +503,27 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
if (messageTransactionData.isText())
continue;
// We're expecting: HASH160(secret-A), Alice's Dogecoin pubkeyhash and lockTime-A
// We're expecting: HASH160(secret-A), Alice's PirateChain pubkeyhash and lockTime-A
byte[] messageData = messageTransactionData.getData();
DogecoinACCTv2.OfferMessageData offerMessageData = DogecoinACCTv2.extractOfferMessageData(messageData);
PirateChainACCTv3.OfferMessageData offerMessageData = PirateChainACCTv3.extractOfferMessageData(messageData);
if (offerMessageData == null)
continue;
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDogecoinPKH;
byte[] aliceForeignPublicKey = offerMessageData.partnerPirateChainPublicKey;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA;
long messageTimestamp = messageTransactionData.getTimestamp();
int refundTimeout = DogecoinACCTv2.calcRefundTimeout(messageTimestamp, lockTimeA);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
// Determine P2SH-A address and confirm funded
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
byte[] redeemScriptA = PirateChainHTLC.buildScript(aliceForeignPublicKey, lockTimeA, tradeBotData.getTradeForeignPublicKey(), hashOfSecretA);
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
@@ -521,7 +535,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
case REDEEMED:
// We've already redeemed this?
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddress));
return;
case REFUND_IN_PROGRESS:
@@ -539,7 +553,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
byte[] outgoingMessageData = DogecoinACCTv2.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
byte[] outgoingMessageData = PirateChainACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKey, hashOfSecretA, lockTimeA, refundTimeout);
String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
@@ -578,7 +592,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
* <p>
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
* <p>
* In revealing a valid secret-A, Bob can then redeem the DOGE funds from P2SH-A.
* In revealing a valid secret-A, Bob can then redeem the ARRR funds from P2SH-A.
* <p>
* @throws ForeignBlockchainException
*/
@@ -587,19 +601,19 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
return;
Dogecoin dogecoin = Dogecoin.getInstance();
PirateChain pirateChain = PirateChain.getInstance();
int lockTimeA = tradeBotData.getLockTimeA();
// Refund P2SH-A if we've passed lockTime-A
if (NTP.getTime() >= lockTimeA * 1000L) {
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
byte[] redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
@@ -611,21 +625,21 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
case REDEEMED:
// Already redeemed?
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddress));
return;
case REFUND_IN_PROGRESS:
case REFUNDED:
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddress));
return;
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
() -> atData.getIsFinished()
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddress)
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddress));
return;
}
@@ -645,7 +659,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
}
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
int refundTimeout = DogecoinACCTv2.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
int refundTimeout = PirateChainACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
// Our calculated refundTimeout should match AT's refundTimeout
if (refundTimeout != crossChainTradeData.refundTimeout) {
@@ -659,7 +673,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
// Send 'redeem' MESSAGE to AT using both secret
byte[] secretA = tradeBotData.getSecret();
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
byte[] messageData = DogecoinACCTv2.buildRedeemMessage(secretA, qortalReceivingAddress);
byte[] messageData = PirateChainACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
@@ -686,15 +700,15 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
}
/**
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the DOGE funds from P2SH-A.
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the ARRR funds from P2SH-A.
* <p>
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
* trade-bot is done with this specific trade and finalizes in refunded state.
* <p>
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the DOGE funds from P2SH-A
* to Bob's 'foreign'/Dogecoin trade legacy-format address, as derived from trade private key.
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the ARRR funds from P2SH-A
* to Bob's 'foreign'/PirateChain trade legacy-format address, as derived from trade private key.
* <p>
* (This could potentially be 'improved' to send DOGE to any address of Bob's choosing by changing the transaction output).
* (This could potentially be 'improved' to send ARRR to any address of Bob's choosing by changing the transaction output).
* <p>
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
* @throws ForeignBlockchainException
@@ -708,14 +722,14 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
// If AT is REFUNDED or CANCELLED then something has gone wrong
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the DOGE
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the ARRR
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
return;
}
byte[] secretA = DogecoinACCTv2.getInstance().findSecretA(repository, crossChainTradeData);
byte[] secretA = PirateChainACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
if (secretA == null) {
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
return;
@@ -723,18 +737,21 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
// Use secret-A to redeem P2SH-A
Dogecoin dogecoin = Dogecoin.getInstance();
PirateChain pirateChain = PirateChain.getInstance();
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
int lockTimeA = crossChainTradeData.lockTimeA;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
byte[] redeemScriptA = PirateChainHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); // Use 't3' prefix when refunding
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
String receivingAddress = Bech32.encode("zs", receivingAccountInfo);
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
@@ -753,20 +770,27 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
return;
case FUNDED: {
// Get funding txid
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
if (fundingTxidHex == null) {
throw new ForeignBlockchainException("Missing funding txid when redeeming P2SH");
}
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
// Redeem P2SH
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
byte[] privateKey = tradeBotData.getTradePrivateKey();
String secret58 = Base58.encode(secretA);
String privateKey58 = Base58.encode(privateKey);
String redeemScript58 = Base58.encode(redeemScriptA);
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey,
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
dogecoin.broadcastTransaction(p2shRedeemTransaction);
String txid = PirateChain.getInstance().redeemP2sh(p2shAddressT3, receivingAddress, redeemAmount.value,
redeemScript58, fundingTxid58, secret58, privateKey58);
LOGGER.info("Redeem txid: {}", txid);
break;
}
}
String receivingAddress = dogecoin.pkhToAddress(receivingAccountInfo);
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
}
@@ -783,21 +807,22 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
if (NTP.getTime() <= lockTimeA * 1000L)
return;
Dogecoin dogecoin = Dogecoin.getInstance();
PirateChain pirateChain = PirateChain.getInstance();
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
int medianBlockTime = dogecoin.getMedianBlockTime();
int medianBlockTime = pirateChain.getMedianBlockTime();
if (medianBlockTime <= lockTimeA)
return;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
byte[] redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status
String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); // Use 't3' prefix when refunding
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
BitcoinyHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
@@ -809,7 +834,7 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
case REDEEMED:
// Too late!
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("P2SH-A %s already spent!", p2shAddressA));
() -> String.format("P2SH-A %s already spent!", p2shAddress));
return;
case REFUND_IN_PROGRESS:
@@ -817,24 +842,28 @@ public class DogecoinACCTv2TradeBot implements AcctTradeBot {
break;
case FUNDED:{
// Get funding txid
String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA);
if (fundingTxidHex == null) {
throw new ForeignBlockchainException("Missing funding txid when refunding P2SH");
}
String fundingTxid58 = Base58.encode(HashCode.fromString(fundingTxidHex).asBytes());
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
byte[] privateKey = tradeBotData.getTradePrivateKey();
String privateKey58 = Base58.encode(privateKey);
String redeemScript58 = Base58.encode(redeemScriptA);
String receivingAddress = pirateChain.getWalletAddress(tradeBotData.getForeignKey());
// Determine receive address for refund
String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
Address receiving = Address.fromString(dogecoin.getNetworkParameters(), receiveAddress);
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(dogecoin.getNetworkParameters(), refundAmount, refundKey,
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
dogecoin.broadcastTransaction(p2shRefundTransaction);
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
receivingAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTimeA, privateKey58);
LOGGER.info("Refund txid: {}", txid);
break;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddress));
}
/**

View File

@@ -94,14 +94,14 @@ public class TradeBot implements Listener {
private static final Map<Class<? extends ACCT>, Supplier<AcctTradeBot>> acctTradeBotSuppliers = new HashMap<>();
static {
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(BitcoinACCTv3.class, BitcoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(LitecoinACCTv2.class, LitecoinACCTv2TradeBot::getInstance);
acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(PirateChainACCTv3.class, PirateChainACCTv3TradeBot::getInstance);
}
private static TradeBot instance;
@@ -241,8 +241,8 @@ public class TradeBot implements Listener {
if (!(event instanceof Synchronizer.NewChainTipEvent))
return;
// Don't process trade bots or broadcast presence timestamps if our chain is more than 30 minutes old
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
// Don't process trade bots or broadcast presence timestamps if our chain is more than 60 minutes old
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
return;
@@ -291,14 +291,14 @@ public class TradeBot implements Listener {
}
public static byte[] deriveTradeNativePublicKey(byte[] privateKey) {
return PrivateKeyAccount.toPublicKey(privateKey);
return Crypto.toPublicKey(privateKey);
}
public static byte[] deriveTradeForeignPublicKey(byte[] privateKey) {
return ECKey.fromPrivate(privateKey).getPubKey();
}
/*package*/ static byte[] generateSecret() {
/*package*/ public static byte[] generateSecret() {
byte[] secret = new byte[32];
RANDOM.nextBytes(secret);
return secret;
@@ -468,9 +468,6 @@ public class TradeBot implements Listener {
List<TradePresenceData> safeTradePresences = List.copyOf(this.safeAllTradePresencesByPubkey.values());
if (safeTradePresences.isEmpty())
return;
LOGGER.debug("Broadcasting all {} known trade presences. Next broadcast timestamp: {}",
safeTradePresences.size(), nextTradePresenceBroadcastTimestamp
);
@@ -637,7 +634,7 @@ public class TradeBot implements Listener {
}
if (newCount > 0) {
LOGGER.debug("New trade presences: {}", newCount);
LOGGER.debug("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
rebuildSafeAllTradePresences();
}
}

View File

@@ -7,6 +7,7 @@ import java.util.Map;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
@@ -18,10 +19,12 @@ public class Bitcoin extends Bitcoiny {
public static final String CURRENCY_CODE = "BTC";
private static final long MINIMUM_ORDER_AMOUNT = 100000; // 0.001 BTC minimum order, due to high fees
// 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 NEW_FEE_AMOUNT = 6_000L;
private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
@@ -171,6 +174,8 @@ public class Bitcoin extends Bitcoiny {
Context bitcoinjContext = new Context(bitcoinNet.getParams());
instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
electrumX.setBlockchain(instance);
}
return instance;
@@ -182,6 +187,11 @@ public class Bitcoin extends Bitcoiny {
instance = null;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
}
// Actual useful methods for use by other classes
/**
@@ -195,4 +205,17 @@ public class Bitcoin extends Bitcoiny {
return this.bitcoinNet.getP2shFee(timestamp);
}
/**
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt> using 20 sat/byte fee.
*
* @param xprv58 BIP32 private key
* @param recipient P2PKH address
* @param amount unscaled amount
* @return transaction, or null if insufficient funds
*/
@Override
public Transaction buildSpend(String xprv58, String recipient, long amount) {
return buildSpend(xprv58, recipient, amount, 20L);
}
}

View File

@@ -2,6 +2,8 @@ package org.qortal.crosschain;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.*;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
@@ -27,7 +29,7 @@ import static org.ciyam.at.OpCode.calcOffset;
*
* <p>
* <ul>
* <li>Bob generates Litecoin & Qortal 'trade' keys
* <li>Bob generates Bitcoin & Qortal 'trade' keys
* <ul>
* <li>private key required to sign P2SH redeem tx</li>
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li>
@@ -40,12 +42,12 @@ import static org.ciyam.at.OpCode.calcOffset;
* </li>
* <li>Alice finds Qortal AT and wants to trade
* <ul>
* <li>Alice generates Litecoin & Qortal 'trade' keys</li>
* <li>Alice funds Litecoin P2SH-A</li>
* <li>Alice generates Bitcoin & Qortal 'trade' keys</li>
* <li>Alice funds Bitcoin P2SH-A</li>
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
* <ul>
* <li>hash-of-secret-A</li>
* <li>her 'trade' Litecoin PKH</li>
* <li>her 'trade' Bitcoin PKH</li>
* </ul>
* </li>
* </ul>
@@ -56,7 +58,7 @@ import static org.ciyam.at.OpCode.calcOffset;
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
* <ul>
* <li>Alice's trade Qortal address</li>
* <li>Alice's trade Litecoin PKH</li>
* <li>Alice's trade Bitcoin PKH</li>
* <li>hash-of-secret-A</li>
* </ul>
* </li>
@@ -75,16 +77,18 @@ import static org.ciyam.at.OpCode.calcOffset;
* </li>
* <li>Bob checks AT, extracts secret-A
* <ul>
* <li>Bob redeems P2SH-A using his Litecoin trade key and secret-A</li>
* <li>P2SH-A LTC funds end up at Litecoin address determined by redeem transaction output(s)</li>
* <li>Bob redeems P2SH-A using his Bitcoin trade key and secret-A</li>
* <li>P2SH-A BTC funds end up at Bitcoin address determined by redeem transaction output(s)</li>
* </ul>
* </li>
* </ul>
*/
public class LitecoinACCTv2 implements ACCT {
public class BitcoinACCTv3 implements ACCT {
public static final String NAME = LitecoinACCTv2.class.getSimpleName();
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("d5ea386a41441180c854ca8d7bbc620bfd53a97df2650a2b162b52324caf6e19").asBytes(); // SHA256 of AT code bytes
private static final Logger LOGGER = LogManager.getLogger(BitcoinACCTv3.class);
public static final String NAME = BitcoinACCTv3.class.getSimpleName();
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("676fb9350708dafa054eb0262d655039e393c1eb4918ec582f8d45524c9b4860").asBytes(); // SHA256 of AT code bytes
public static final int SECRET_LENGTH = 32;
@@ -94,27 +98,27 @@ public class LitecoinACCTv2 implements ACCT {
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
public static class OfferMessageData {
public byte[] partnerLitecoinPKH;
public byte[] partnerBitcoinPKH;
public byte[] hashOfSecretA;
public long lockTimeA;
}
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerLitecoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerBitcoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
+ 24 /*partner's Litecoin PKH (padded from 20 to 24)*/
+ 24 /*partner's Bitcoin PKH (padded from 20 to 24)*/
+ 8 /*AT trade timeout (minutes)*/
+ 24 /*hash of secret-A (padded from 20 to 24)*/
+ 8 /*lockTimeA*/;
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
private static LitecoinACCTv2 instance;
private static BitcoinACCTv3 instance;
private LitecoinACCTv2() {
private BitcoinACCTv3() {
}
public static synchronized LitecoinACCTv2 getInstance() {
public static synchronized BitcoinACCTv3 getInstance() {
if (instance == null)
instance = new LitecoinACCTv2();
instance = new BitcoinACCTv3();
return instance;
}
@@ -131,7 +135,7 @@ public class LitecoinACCTv2 implements ACCT {
@Override
public ForeignBlockchain getBlockchain() {
return Litecoin.getInstance();
return Bitcoin.getInstance();
}
/**
@@ -141,14 +145,14 @@ public class LitecoinACCTv2 implements ACCT {
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
*
* @param creatorTradeAddress AT creator's trade Qortal address
* @param litecoinPublicKeyHash 20-byte HASH160 of creator's trade Litecoin public key
* @param bitcoinPublicKeyHash 20-byte HASH160 of creator's trade Bitcoin public key
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
* @param litecoinAmount how much LTC the AT creator is expecting to trade
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
* @param tradeTimeout suggested timeout for entire trade
*/
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] litecoinPublicKeyHash, long qortAmount, long litecoinAmount, int tradeTimeout) {
if (litecoinPublicKeyHash.length != 20)
throw new IllegalArgumentException("Litecoin public key hash should be 20 bytes");
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, long qortAmount, long bitcoinAmount, int tradeTimeout) {
if (bitcoinPublicKeyHash.length != 20)
throw new IllegalArgumentException("Bitcoin public key hash should be 20 bytes");
// Labels for data segment addresses
int addrCounter = 0;
@@ -160,11 +164,11 @@ public class LitecoinACCTv2 implements ACCT {
final int addrCreatorTradeAddress3 = addrCounter++;
final int addrCreatorTradeAddress4 = addrCounter++;
final int addrLitecoinPublicKeyHash = addrCounter;
final int addrBitcoinPublicKeyHash = addrCounter;
addrCounter += 4;
final int addrQortAmount = addrCounter++;
final int addrLitecoinAmount = addrCounter++;
final int addrBitcoinAmount = addrCounter++;
final int addrTradeTimeout = addrCounter++;
final int addrMessageTxnType = addrCounter++;
@@ -175,8 +179,8 @@ public class LitecoinACCTv2 implements ACCT {
final int addrQortalPartnerAddressPointer = addrCounter++;
final int addrMessageSenderPointer = addrCounter++;
final int addrTradeMessagePartnerLitecoinPKHOffset = addrCounter++;
final int addrPartnerLitecoinPKHPointer = addrCounter++;
final int addrTradeMessagePartnerBitcoinPKHOffset = addrCounter++;
final int addrPartnerBitcoinPKHPointer = addrCounter++;
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
final int addrHashOfSecretAPointer = addrCounter++;
@@ -222,7 +226,7 @@ public class LitecoinACCTv2 implements ACCT {
final int addrHashOfSecretA = addrCounter;
addrCounter += 4;
final int addrPartnerLitecoinPKH = addrCounter;
final int addrPartnerBitcoinPKH = addrCounter;
addrCounter += 4;
final int addrPartnerReceivingAddress = addrCounter;
@@ -239,17 +243,17 @@ public class LitecoinACCTv2 implements ACCT {
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
// Litecoin public key hash
assert dataByteBuffer.position() == addrLitecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrLitecoinPublicKeyHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(litecoinPublicKeyHash, 32, 0));
// Bitcoin public key hash
assert dataByteBuffer.position() == addrBitcoinPublicKeyHash * MachineState.VALUE_SIZE : "addrBitcoinPublicKeyHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(bitcoinPublicKeyHash, 32, 0));
// Redeem Qort amount
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
dataByteBuffer.putLong(qortAmount);
// Expected Litecoin amount
assert dataByteBuffer.position() == addrLitecoinAmount * MachineState.VALUE_SIZE : "addrLitecoinAmount incorrect";
dataByteBuffer.putLong(litecoinAmount);
// Expected Bitcoin amount
assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect";
dataByteBuffer.putLong(bitcoinAmount);
// Suggested trade timeout (minutes)
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
@@ -279,13 +283,13 @@ public class LitecoinACCTv2 implements ACCT {
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
dataByteBuffer.putLong(addrMessageSender1);
// Offset into 'trade' MESSAGE data payload for extracting partner's Litecoin PKH
assert dataByteBuffer.position() == addrTradeMessagePartnerLitecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerLitecoinPKHOffset incorrect";
// Offset into 'trade' MESSAGE data payload for extracting partner's Bitcoin PKH
assert dataByteBuffer.position() == addrTradeMessagePartnerBitcoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerBitcoinPKHOffset incorrect";
dataByteBuffer.putLong(32L);
// Index into data segment of partner's Litecoin PKH, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerLitecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerLitecoinPKHPointer incorrect";
dataByteBuffer.putLong(addrPartnerLitecoinPKH);
// Index into data segment of partner's Bitcoin PKH, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerBitcoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerBitcoinPKHPointer incorrect";
dataByteBuffer.putLong(addrPartnerBitcoinPKH);
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
@@ -422,10 +426,10 @@ public class LitecoinACCTv2 implements ACCT {
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
// Extract trade partner's Litecoin public key hash (PKH) from message into B
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerLitecoinPKHOffset));
// Store partner's Litecoin PKH (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerLitecoinPKHPointer));
// Extract trade partner's Bitcoin public key hash (PKH) from message into B
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerBitcoinPKHOffset));
// Store partner's Bitcoin PKH (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerBitcoinPKHPointer));
// Extract AT trade timeout (minutes) (from B4)
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
@@ -458,9 +462,6 @@ public class LitecoinACCTv2 implements ACCT {
/* Transaction processing loop */
labelRedeemTxnLoop = codeByteBuffer.position();
/* Sleep until message arrives */
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
@@ -534,12 +535,15 @@ public class LitecoinACCTv2 implements ACCT {
/* Refund balance back to AT creator */
labelRefund = codeByteBuffer.position();
/* NOP - to ensure BITCOIN ACCT is unique */
codeByteBuffer.put(OpCode.NOP.compile());
// Set refunded mode
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile LTC-QORT ACCT?", e);
throw new IllegalStateException("Unable to compile BTC-QORT ACCT?", e);
}
}
@@ -548,7 +552,7 @@ public class LitecoinACCTv2 implements ACCT {
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
assert Arrays.equals(Crypto.digest(codeBytes), LitecoinACCTv2.CODE_BYTES_HASH)
assert Arrays.equals(Crypto.digest(codeBytes), BitcoinACCTv3.CODE_BYTES_HASH)
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
final short ciyamAtVersion = 2;
@@ -586,7 +590,7 @@ public class LitecoinACCTv2 implements ACCT {
CrossChainTradeData tradeData = new CrossChainTradeData();
tradeData.foreignBlockchain = SupportedBlockchain.LITECOIN.name();
tradeData.foreignBlockchain = SupportedBlockchain.BITCOIN.name();
tradeData.acctName = NAME;
tradeData.qortalAtAddress = atAddress;
@@ -607,7 +611,7 @@ public class LitecoinACCTv2 implements ACCT {
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
// Creator's Litecoin/foreign public key hash
// Creator's Bitcoin/foreign public key hash
tradeData.creatorForeignPKH = new byte[20];
dataByteBuffer.get(tradeData.creatorForeignPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
@@ -618,7 +622,7 @@ public class LitecoinACCTv2 implements ACCT {
// Redeem payout
tradeData.qortAmount = dataByteBuffer.getLong();
// Expected LTC amount
// Expected BTC amount
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
// Trade timeout
@@ -642,10 +646,10 @@ public class LitecoinACCTv2 implements ACCT {
// Skip pointer to message sender
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for partner's Litecoin PKH
// Skip 'trade' message data offset for partner's Bitcoin PKH
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Litecoin PKH
// Skip pointer to partner's Bitcoin PKH
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for hash-of-secret-A
@@ -711,10 +715,10 @@ public class LitecoinACCTv2 implements ACCT {
dataByteBuffer.get(hashOfSecretA);
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
// Potential partner's Litecoin PKH
byte[] partnerLitecoinPKH = new byte[20];
dataByteBuffer.get(partnerLitecoinPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerLitecoinPKH.length); // skip to 32 bytes
// Potential partner's Bitcoin PKH
byte[] partnerBitcoinPKH = new byte[20];
dataByteBuffer.get(partnerBitcoinPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerBitcoinPKH.length); // skip to 32 bytes
// Partner's receiving address (if present)
byte[] partnerReceivingAddress = new byte[25];
@@ -733,7 +737,7 @@ public class LitecoinACCTv2 implements ACCT {
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
tradeData.qortalPartnerAddress = qortalRecipient;
tradeData.hashOfSecretA = hashOfSecretA;
tradeData.partnerForeignPKH = partnerLitecoinPKH;
tradeData.partnerForeignPKH = partnerBitcoinPKH;
tradeData.lockTimeA = lockTimeA;
if (mode == AcctMode.REDEEMED)
@@ -759,7 +763,7 @@ public class LitecoinACCTv2 implements ACCT {
return null;
OfferMessageData offerMessageData = new OfferMessageData();
offerMessageData.partnerLitecoinPKH = Arrays.copyOfRange(messageData, 0, 20);
offerMessageData.partnerBitcoinPKH = Arrays.copyOfRange(messageData, 0, 20);
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);

View File

@@ -29,6 +29,7 @@ import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.qortal.api.model.SimpleForeignTransaction;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.utils.Amounts;
import org.qortal.utils.BitTwiddling;
@@ -42,7 +43,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
public static final int HASH160_LENGTH = 20;
protected final BitcoinyBlockchainProvider blockchain;
protected final BitcoinyBlockchainProvider blockchainProvider;
protected final Context bitcoinjContext;
protected final String currencyCode;
@@ -61,18 +62,13 @@ public abstract class Bitcoiny implements ForeignBlockchain {
/** How many wallet keys to generate in each batch. */
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3;
/** How many wallet keys to generate when using bitcoinj as the data provider.
* We must use a higher value here since we are unable to request multiple batches of keys.
* Without this, the bitcoinj state can be missing transactions, causing errors such as "insufficient balance". */
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ = 50;
/** Byte offset into raw block headers to block timestamp. */
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
// Constructors and instance
protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
this.blockchain = blockchain;
protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode) {
this.blockchainProvider = blockchainProvider;
this.bitcoinjContext = bitcoinjContext;
this.currencyCode = currencyCode;
@@ -82,7 +78,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// Getters & setters
public BitcoinyBlockchainProvider getBlockchainProvider() {
return this.blockchain;
return this.blockchainProvider;
}
public Context getBitcoinjContext() {
@@ -155,10 +151,10 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if error occurs
*/
public int getMedianBlockTime() throws ForeignBlockchainException {
int height = this.blockchain.getCurrentHeight();
int height = this.blockchainProvider.getCurrentHeight();
// Grab latest 11 blocks
List<byte[]> blockHeaders = this.blockchain.getRawBlockHeaders(height - 11, 11);
List<byte[]> blockHeaders = this.blockchainProvider.getRawBlockHeaders(height - 11, 11);
if (blockHeaders.size() < 11)
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
@@ -197,7 +193,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if there was an error
*/
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
return this.blockchain.getConfirmedBalance(addressToScriptPubKey(base58Address));
return this.blockchainProvider.getConfirmedBalance(addressToScriptPubKey(base58Address));
}
/**
@@ -208,7 +204,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
*/
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
public List<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (UnspentOutput unspentOutput : unspentOutputs) {
@@ -228,7 +224,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
*/
// TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
public List<TransactionOutput> getOutputs(byte[] txHash) throws ForeignBlockchainException {
byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash);
byte[] rawTransactionBytes = this.blockchainProvider.getRawTransaction(txHash);
Context.propagate(bitcoinjContext);
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
@@ -245,7 +241,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
ForeignBlockchainException e2 = null;
while (retries <= 3) {
try {
return this.blockchain.getAddressTransactions(scriptPubKey, includeUnconfirmed);
return this.blockchainProvider.getAddressTransactions(scriptPubKey, includeUnconfirmed);
} catch (ForeignBlockchainException e) {
e2 = e;
retries++;
@@ -261,7 +257,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if there was an error.
*/
public List<TransactionHash> getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
return this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
return this.blockchainProvider.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
}
/**
@@ -270,11 +266,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if there was an error
*/
public List<byte[]> getAddressTransactions(String base58Address) throws ForeignBlockchainException {
List<TransactionHash> transactionHashes = this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), false);
List<TransactionHash> transactionHashes = this.blockchainProvider.getAddressTransactions(addressToScriptPubKey(base58Address), false);
List<byte[]> rawTransactions = new ArrayList<>();
for (TransactionHash transactionInfo : transactionHashes) {
byte[] rawTransaction = this.blockchain.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
byte[] rawTransaction = this.blockchainProvider.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
rawTransactions.add(rawTransaction);
}
@@ -292,7 +288,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
ForeignBlockchainException e2 = null;
while (retries <= 3) {
try {
return this.blockchain.getTransaction(txHash);
return this.blockchainProvider.getTransaction(txHash);
} catch (ForeignBlockchainException e) {
e2 = e;
retries++;
@@ -307,7 +303,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @throws ForeignBlockchainException if error occurs
*/
public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
this.blockchain.broadcastTransaction(transaction.bitcoinSerialize());
this.blockchainProvider.broadcastTransaction(transaction.bitcoinSerialize());
}
/**
@@ -360,22 +356,25 @@ public abstract class Bitcoiny implements ForeignBlockchain {
* @param key58 BIP32/HD extended Bitcoin private/public key
* @return unspent BTC balance, or null if unable to determine balance
*/
public Long getWalletBalance(String key58) {
Context.propagate(bitcoinjContext);
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
// It's more accurate to calculate the balance from the transactions, rather than asking Bitcoinj
return this.getWalletBalanceFromTransactions(key58);
Wallet wallet = walletFromDeterministicKey58(key58);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
Coin balance = wallet.getBalance();
if (balance == null)
return null;
return balance.value;
// Context.propagate(bitcoinjContext);
//
// Wallet wallet = walletFromDeterministicKey58(key58);
// wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
//
// Coin balance = wallet.getBalance();
// if (balance == null)
// return null;
//
// return balance.value;
}
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
long balance = 0;
Comparator<SimpleTransaction> oldestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp);
Comparator<SimpleTransaction> oldestTimestampFirstComparator = Comparator.comparingLong(SimpleTransaction::getTimestamp);
List<SimpleTransaction> transactions = getWalletTransactions(key58).stream().sorted(oldestTimestampFirstComparator).collect(Collectors.toList());
for (SimpleTransaction transaction : transactions) {
balance += transaction.getTotalAmount();
@@ -409,9 +408,6 @@ public abstract class Bitcoiny implements ForeignBlockchain {
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
Set<String> keySet = new HashSet<>();
// Set the number of consecutive empty batches required before giving up
final int numberOfAdditionalBatchesToSearch = 7;
int unusedCounter = 0;
int ki = 0;
do {
@@ -438,12 +434,12 @@ public abstract class Bitcoiny implements ForeignBlockchain {
if (areAllKeysUnused) {
// No transactions
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
// ... and we've hit our search limit
break;
}
// We haven't hit our search limit yet so increment the counter and keep looking
unusedCounter++;
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
} else {
// Some keys in this batch were used, so reset the counter
unusedCounter = 0;
@@ -455,7 +451,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// Process new keys
} while (true);
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingLong(SimpleTransaction::getTimestamp).reversed();
// Update cache and return
transactionsCacheTimestamp = NTP.getTime();
@@ -537,7 +533,8 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// All inputs and outputs relate to this wallet, so the balance should be unaffected
amount = 0;
}
return new SimpleTransaction(t.txHash, t.timestamp, amount, fee, inputs, outputs);
long timestampMillis = t.timestamp * 1000L;
return new SimpleTransaction(t.txHash, timestampMillis, amount, fee, inputs, outputs, null);
}
/**
@@ -573,7 +570,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = this.blockchain.getUnspentOutputs(script, false);
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(script, false);
/*
* If there are no unspent outputs then either:
@@ -591,7 +588,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
List<TransactionHash> historicTransactionHashes = this.blockchainProvider.getAddressTransactions(script, false);
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
@@ -629,7 +626,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
this.keyChain = this.wallet.getActiveKeyChain();
// Set up wallet's key chain
this.keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ);
this.keyChain.setLookaheadSize(Settings.getInstance().getBitcoinjLookaheadSize());
this.keyChain.maybeLookAhead();
}
@@ -650,7 +647,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
List<UnspentOutput> unspentOutputs;
try {
unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false);
unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false);
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
}
@@ -674,7 +671,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes;
try {
historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false);
historicTransactionHashes = this.bitcoiny.blockchainProvider.getAddressTransactions(script, false);
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
}
@@ -727,7 +724,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
@Override
public int getChainHeadHeight() throws UTXOProviderException {
try {
return this.bitcoiny.blockchain.getCurrentHeight();
return this.bitcoiny.blockchainProvider.getCurrentHeight();
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
}

View File

@@ -1,5 +1,7 @@
package org.qortal.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import java.util.List;
public abstract class BitcoinyBlockchainProvider {
@@ -7,18 +9,32 @@ public abstract class BitcoinyBlockchainProvider {
public static final boolean INCLUDE_UNCONFIRMED = true;
public static final boolean EXCLUDE_UNCONFIRMED = false;
/** Sets the blockchain using this provider instance */
public abstract void setBlockchain(Bitcoiny blockchain);
/** Returns ID unique to bitcoiny network (e.g. "Litecoin-TEST3") */
public abstract String getNetId();
/** Returns current blockchain height. */
public abstract int getCurrentHeight() throws ForeignBlockchainException;
/** Returns a list of compact blocks, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max.
* Used for Pirate/Zcash only. If ever needed for other blockchains, the response format will need to be
* made generic. */
public abstract List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException;
/** Returns a list of raw block headers, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
public abstract List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException;
/** Returns a list of block timestamps, starting at <tt>startHeight</tt> (inclusive), up to <tt>count</tt> max. */
public abstract List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException;
/** Returns balance of address represented by <tt>scriptPubKey</tt>. */
public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException;
/** Returns balance of base58 encoded address. */
public abstract long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException;
/** Returns raw, serialized, transaction bytes given <tt>txHash</tt>. */
public abstract byte[] getRawTransaction(String txHash) throws ForeignBlockchainException;
@@ -31,6 +47,12 @@ public abstract class BitcoinyBlockchainProvider {
/** Returns list of transaction hashes (and heights) for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
public abstract List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
/** Returns list of BitcoinyTransaction objects for <tt>address</tt>, optionally including unconfirmed transactions. */
public abstract List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException;
/** Returns list of unspent transaction outputs for <tt>address</tt>, optionally including unconfirmed transactions. */
public abstract List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException;
/** Returns list of unspent transaction outputs for address represented by <tt>scriptPubKey</tt>, optionally including unconfirmed transactions. */
public abstract List<UnspentOutput> getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;

View File

@@ -1,5 +1,6 @@
package org.qortal.crosschain;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@@ -10,8 +11,13 @@ import javax.xml.bind.annotation.XmlTransient;
@XmlAccessorType(XmlAccessType.FIELD)
public class BitcoinyTransaction {
public static final Comparator<BitcoinyTransaction> CONFIRMED_FIRST = (a, b) -> Boolean.compare(a.height != 0, b.height != 0);
public final String txHash;
@XmlTransient
public Integer height;
@XmlTransient
public final int size;
@@ -113,6 +119,10 @@ public class BitcoinyTransaction {
this.totalAmount = outputs.stream().map(output -> output.value).reduce(0L, Long::sum);
}
public int getHeight() {
return this.height;
}
public String toString() {
return String.format("txHash %s, size %d, locktime %d, timestamp %d\n"
+ "\tinputs: [%s]\n"

View File

@@ -135,6 +135,8 @@ public class Dogecoin extends Bitcoiny {
Context bitcoinjContext = new Context(dogecoinNet.getParams());
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
electrumX.setBlockchain(instance);
}
return instance;

View File

@@ -11,6 +11,7 @@ import java.util.regex.Pattern;
import javax.net.ssl.SSLSocketFactory;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONArray;
@@ -107,6 +108,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
private final String netId;
private final String expectedGenesisHash;
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
private Bitcoiny blockchain;
private final Object serverLock = new Object();
private Server currentServer;
@@ -135,6 +137,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
// Methods for use by other classes
@Override
public void setBlockchain(Bitcoiny blockchain) {
this.blockchain = blockchain;
}
@Override
public String getNetId() {
return this.netId;
@@ -161,6 +168,16 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return ((Long) heightObj).intValue();
}
/**
* Returns list of raw blocks, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
throw new ForeignBlockchainException("getCompactBlocks not implemented for ElectrumX due to being specific to zcash");
}
/**
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
* <p>
@@ -222,6 +239,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return rawBlockHeaders;
}
/**
* Returns list of raw block timestamps, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException {
// FUTURE: implement this if needed. For now we use getRawBlockHeaders directly
throw new ForeignBlockchainException("getBlockTimestamps not yet implemented for ElectrumX");
}
/**
* Returns confirmed balance, based on passed payment script.
* <p>
@@ -247,6 +275,29 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return (Long) balanceJson.get("confirmed");
}
/**
* Returns confirmed balance, based on passed base58 encoded address.
* <p>
* @return confirmed balance, or zero if address unknown
* @throws ForeignBlockchainException if there was an error
*/
@Override
public long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException {
throw new ForeignBlockchainException("getConfirmedAddressBalance not yet implemented for ElectrumX");
}
/**
* Returns list of unspent outputs pertaining to passed address.
* <p>
* @return list of unspent outputs, or empty list if address unknown
* @throws ForeignBlockchainException if there was an error.
*/
@Override
public List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
byte[] script = this.blockchain.addressToScriptPubKey(address);
return this.getUnspentOutputs(script, includeUnconfirmed);
}
/**
* Returns list of unspent outputs pertaining to passed payment script.
* <p>
@@ -482,6 +533,12 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
return transactionHashes;
}
@Override
public List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
// FUTURE: implement this if needed. For now we use getAddressTransactions() + getTransaction()
throw new ForeignBlockchainException("getAddressBitcoinyTransactions not yet implemented for ElectrumX");
}
/**
* Broadcasts raw transaction to network.
* <p>
@@ -682,6 +739,10 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
} catch (IOException | NoSuchElementException e) {
// Unable to send, or receive -- try another server?
return null;
} catch (NoSuchMethodError e) {
// Likely an SSL dependency issue - retries are unlikely to succeed
LOGGER.error("ElectrumX output stream error", e);
return null;
}
long endTime = System.currentTimeMillis();

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2011 Google Inc.
* Copyright 2014 Giannis Dzegoutanis
* Copyright 2015 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Updated for Zcash in May 2022 by Qortal core dev team. Modifications allow
* correct encoding of P2SH (t3) addresses only. */
package org.qortal.crosschain;
import org.bitcoinj.core.*;
import org.bitcoinj.params.Networks;
import org.bitcoinj.script.Script.ScriptType;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Objects;
/**
* <p>A Bitcoin address looks like 1MsScoe2fTJoq4ZPdQgqyhgWeoNamYPevy and is derived from an elliptic curve public key
* plus a set of network parameters. Not to be confused with a {@link PeerAddress} or {@link AddressMessage}
* which are about network (TCP) addresses.</p>
*
* <p>A standard address is built by taking the RIPE-MD160 hash of the public key bytes, with a version prefix and a
* checksum suffix, then encoding it textually as base58. The version prefix is used to both denote the network for
* which the address is valid (see {@link NetworkParameters}, and also to indicate how the bytes inside the address
* should be interpreted. Whilst almost all addresses today are hashes of public keys, another (currently unsupported
* type) can contain a hash of a script instead.</p>
*/
public class LegacyZcashAddress extends Address {
/**
* An address is a RIPEMD160 hash of a public key, therefore is always 160 bits or 20 bytes.
*/
public static final int LENGTH = 20;
/** True if P2SH, false if P2PKH. */
public final boolean p2sh;
/* Zcash P2SH header bytes */
private static int P2SH_HEADER_1 = 28;
private static int P2SH_HEADER_2 = 189;
/**
* Private constructor. Use {@link #fromBase58(NetworkParameters, String)},
* {@link #fromPubKeyHash(NetworkParameters, byte[])}, {@link #fromScriptHash(NetworkParameters, byte[])} or
* {@link #fromKey(NetworkParameters, ECKey)}.
*
* @param params
* network this address is valid for
* @param p2sh
* true if hash160 is hash of a script, false if it is hash of a pubkey
* @param hash160
* 20-byte hash of pubkey or script
*/
private LegacyZcashAddress(NetworkParameters params, boolean p2sh, byte[] hash160) throws AddressFormatException {
super(params, hash160);
if (hash160.length != 20)
throw new AddressFormatException.InvalidDataLength(
"Legacy addresses are 20 byte (160 bit) hashes, but got: " + hash160.length);
this.p2sh = p2sh;
}
/**
* Construct a {@link LegacyZcashAddress} that represents the given pubkey hash. The resulting address will be a P2PKH type of
* address.
*
* @param params
* network this address is valid for
* @param hash160
* 20-byte pubkey hash
* @return constructed address
*/
public static LegacyZcashAddress fromPubKeyHash(NetworkParameters params, byte[] hash160) throws AddressFormatException {
return new LegacyZcashAddress(params, false, hash160);
}
/**
* Construct a {@link LegacyZcashAddress} that represents the public part of the given {@link ECKey}. Note that an address is
* derived from a hash of the public key and is not the public key itself.
*
* @param params
* network this address is valid for
* @param key
* only the public part is used
* @return constructed address
*/
public static LegacyZcashAddress fromKey(NetworkParameters params, ECKey key) {
return fromPubKeyHash(params, key.getPubKeyHash());
}
/**
* Construct a {@link LegacyZcashAddress} that represents the given P2SH script hash.
*
* @param params
* network this address is valid for
* @param hash160
* P2SH script hash
* @return constructed address
*/
public static LegacyZcashAddress fromScriptHash(NetworkParameters params, byte[] hash160) throws AddressFormatException {
return new LegacyZcashAddress(params, true, hash160);
}
/**
* Construct a {@link LegacyZcashAddress} from its base58 form.
*
* @param params
* expected network this address is valid for, or null if if the network should be derived from the
* base58
* @param base58
* base58-encoded textual form of the address
* @throws AddressFormatException
* if the given base58 doesn't parse or the checksum is invalid
* @throws AddressFormatException.WrongNetwork
* if the given address is valid but for a different chain (eg testnet vs mainnet)
*/
public static LegacyZcashAddress fromBase58(@Nullable NetworkParameters params, String base58)
throws AddressFormatException, AddressFormatException.WrongNetwork {
byte[] versionAndDataBytes = Base58.decodeChecked(base58);
int version = versionAndDataBytes[0] & 0xFF;
byte[] bytes = Arrays.copyOfRange(versionAndDataBytes, 1, versionAndDataBytes.length);
if (params == null) {
for (NetworkParameters p : Networks.get()) {
if (version == p.getAddressHeader())
return new LegacyZcashAddress(p, false, bytes);
else if (version == p.getP2SHHeader())
return new LegacyZcashAddress(p, true, bytes);
}
throw new AddressFormatException.InvalidPrefix("No network found for " + base58);
} else {
if (version == params.getAddressHeader())
return new LegacyZcashAddress(params, false, bytes);
else if (version == params.getP2SHHeader())
return new LegacyZcashAddress(params, true, bytes);
throw new AddressFormatException.WrongNetwork(version);
}
}
/**
* Get the version header of an address. This is the first byte of a base58 encoded address.
*
* @return version header as one byte
*/
public int getVersion() {
return p2sh ? params.getP2SHHeader() : params.getAddressHeader();
}
/**
* Returns the base58-encoded textual form, including version and checksum bytes.
*
* @return textual form
*/
public String toBase58() {
return this.encodeChecked(getVersion(), bytes);
}
/** The (big endian) 20 byte hash that is the core of a Bitcoin address. */
@Override
public byte[] getHash() {
return bytes;
}
/**
* Get the type of output script that will be used for sending to the address. This is either
* {@link ScriptType#P2PKH} or {@link ScriptType#P2SH}.
*
* @return type of output script
*/
@Override
public ScriptType getOutputScriptType() {
return p2sh ? ScriptType.P2SH : ScriptType.P2PKH;
}
/**
* Given an address, examines the version byte and attempts to find a matching NetworkParameters. If you aren't sure
* which network the address is intended for (eg, it was provided by a user), you can use this to decide if it is
* compatible with the current wallet.
*
* @return network the address is valid for
* @throws AddressFormatException if the given base58 doesn't parse or the checksum is invalid
*/
public static NetworkParameters getParametersFromAddress(String address) throws AddressFormatException {
return LegacyZcashAddress.fromBase58(null, address).getParameters();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
LegacyZcashAddress other = (LegacyZcashAddress) o;
return super.equals(other) && this.p2sh == other.p2sh;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), p2sh);
}
@Override
public String toString() {
return toBase58();
}
@Override
public LegacyZcashAddress clone() throws CloneNotSupportedException {
return (LegacyZcashAddress) super.clone();
}
public static String encodeChecked(int version, byte[] payload) {
if (version < 0 || version > 255)
throw new IllegalArgumentException("Version not in range.");
// A stringified buffer is:
// 1 byte version + data bytes + 4 bytes check code (a truncated hash)
byte[] addressBytes = new byte[2 + payload.length + 4];
addressBytes[0] = (byte) P2SH_HEADER_1;
addressBytes[1] = (byte) P2SH_HEADER_2;
System.arraycopy(payload, 0, addressBytes, 2, payload.length);
byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length + 2);
System.arraycopy(checksum, 0, addressBytes, payload.length + 2, 4);
return Base58.encode(addressBytes);
}
// // Comparator for LegacyAddress, left argument must be LegacyAddress, right argument can be any Address
// private static final Comparator<Address> LEGACY_ADDRESS_COMPARATOR = Address.PARTIAL_ADDRESS_COMPARATOR
// .thenComparingInt(a -> ((LegacyZcashAddress) a).getVersion()) // Then compare Legacy address version byte
// .thenComparing(a -> a.bytes, UnsignedBytes.lexicographicalComparator()); // Then compare Legacy bytes
//
// /**
// * {@inheritDoc}
// *
// * @param o other {@code Address} object
// * @return comparison result
// */
// @Override
// public int compareTo(Address o) {
// return LEGACY_ADDRESS_COMPARATOR.compare(this, o);
// }
}

View File

@@ -145,6 +145,8 @@ public class Litecoin extends Bitcoiny {
Context bitcoinjContext = new Context(litecoinNet.getParams());
instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
electrumX.setBlockchain(instance);
}
return instance;

View File

@@ -0,0 +1,659 @@
package org.qortal.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats;
import com.google.common.hash.HashCode;
import com.rust.litewalletjni.LiteWalletJni;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.Wallet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.libdohj.params.LitecoinRegTestParams;
import org.libdohj.params.LitecoinTestNet3Params;
import org.libdohj.params.PirateChainMainNetParams;
import org.qortal.api.model.crosschain.PirateChainSendRequest;
import org.qortal.controller.PirateChainWalletController;
import org.qortal.crosschain.PirateLightClient.Server;
import org.qortal.crosschain.PirateLightClient.Server.ConnectionType;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.transform.TransformationException;
import org.qortal.utils.BitTwiddling;
import java.nio.ByteBuffer;
import java.util.*;
public class PirateChain extends Bitcoiny {
public static final String CURRENCY_CODE = "ARRR";
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(10000); // 0.0001 ARRR per 1000 bytes
private static final long MINIMUM_ORDER_AMOUNT = 10000; // 0.0001 ARRR minimum order, to avoid dust errors // TODO: increase this
// Temporary values until a dynamic fee system is written.
private static final long MAINNET_FEE = 10000L; // 0.0001 ARRR
private static final long NON_MAINNET_FEE = 10000L; // 0.0001 ARRR
private static final Map<ConnectionType, Integer> DEFAULT_LITEWALLET_PORTS = new EnumMap<>(ConnectionType.class);
static {
DEFAULT_LITEWALLET_PORTS.put(ConnectionType.TCP, 9067);
DEFAULT_LITEWALLET_PORTS.put(ConnectionType.SSL, 443);
}
public enum PirateChainNet {
MAIN {
@Override
public NetworkParameters getParams() {
return PirateChainMainNetParams.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("arrrlightd.qortal.online", ConnectionType.SSL, 443),
new Server("arrrlightd1.qortal.online", ConnectionType.SSL, 443),
new Server("arrrlightd2.qortal.online", ConnectionType.SSL, 443),
new Server("lightd.pirate.black", ConnectionType.SSL, 443));
}
@Override
public String getGenesisHash() {
return "027e3758c3a65b12aa1046462b486d0a63bfa1beae327897f56c5cfb7daaae71";
}
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
}
},
TEST3 {
@Override
public NetworkParameters getParams() {
return LitecoinTestNet3Params.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList();
}
@Override
public String getGenesisHash() {
return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
},
REGTEST {
@Override
public NetworkParameters getParams() {
return LitecoinRegTestParams.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(
new Server("localhost", ConnectionType.TCP, 9067),
new Server("localhost", ConnectionType.SSL, 443));
}
@Override
public String getGenesisHash() {
// This is unique to each regtest instance
return null;
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
};
public abstract NetworkParameters getParams();
public abstract Collection<Server> getServers();
public abstract String getGenesisHash();
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
}
private static PirateChain instance;
private final PirateChainNet pirateChainNet;
// Constructors and instance
private PirateChain(PirateChainNet pirateChainNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
this.pirateChainNet = pirateChainNet;
LOGGER.info(() -> String.format("Starting Pirate Chain support using %s", this.pirateChainNet.name()));
}
public static synchronized PirateChain getInstance() {
if (instance == null) {
PirateChainNet pirateChainNet = Settings.getInstance().getPirateChainNet();
BitcoinyBlockchainProvider pirateLightClient = new PirateLightClient("PirateChain-" + pirateChainNet.name(), pirateChainNet.getGenesisHash(), pirateChainNet.getServers(), DEFAULT_LITEWALLET_PORTS);
Context bitcoinjContext = new Context(pirateChainNet.getParams());
instance = new PirateChain(pirateChainNet, pirateLightClient, bitcoinjContext, CURRENCY_CODE);
pirateLightClient.setBlockchain(instance);
}
return instance;
}
// Getters & setters
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes
/** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
}
/**
* Returns estimated LTC fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch, or null for 'now'
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
*/
@Override
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
return this.pirateChainNet.getP2shFee(timestamp);
}
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if balance unknown
* @throws ForeignBlockchainException if there was an error
*/
public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
return this.blockchainProvider.getConfirmedAddressBalance(base58Address);
}
/**
* Returns median timestamp from latest 11 blocks, in seconds.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public int getMedianBlockTime() throws ForeignBlockchainException {
int height = this.blockchainProvider.getCurrentHeight();
// Grab latest 11 blocks
List<Long> blockTimestamps = this.blockchainProvider.getBlockTimestamps(height - 11, 11);
if (blockTimestamps.size() < 11)
throw new ForeignBlockchainException("Not enough blocks to determine median block time");
// Descending order
blockTimestamps.sort((a, b) -> Long.compare(b, a));
// Pick median
return Math.toIntExact(blockTimestamps.get(5));
}
/**
* Returns list of compact blocks
* <p>
* @throws ForeignBlockchainException if error occurs
*/
public List<CompactFormats.CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
return this.blockchainProvider.getCompactBlocks(startHeight, count);
}
@Override
public boolean isValidAddress(String address) {
// Start with some simple checks
if (address == null || !address.toLowerCase().startsWith("zs") || address.length() != 78) {
return false;
}
// Now try Bech32 decoding the address (which includes checksum verification)
try {
Bech32.Bech32Data decoded = Bech32.decode(address);
return (decoded != null && Objects.equals("zs", decoded.hrp));
}
catch (AddressFormatException e) {
// Invalid address, checksum failed, etc
return false;
}
}
@Override
public boolean isValidWalletKey(String walletKey) {
// For Pirate Chain, we only care that the key is a random string
// 32 characters in length, as it is used as entropy for the seed.
return walletKey != null && Base58.decode(walletKey).length == 32;
}
/** Returns 't3' prefixed P2SH address using passed redeem script. */
public String deriveP2shAddress(byte[] redeemScriptBytes) {
Context.propagate(bitcoinjContext);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
return LegacyZcashAddress.fromScriptHash(this.params, redeemScriptHash).toString();
}
/** Returns 'b' prefixed P2SH address using passed redeem script. */
public String deriveP2shAddressBPrefix(byte[] redeemScriptBytes) {
Context.propagate(bitcoinjContext);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
return LegacyAddress.fromScriptHash(this.params, redeemScriptHash).toString();
}
public Long getWalletBalance(String entropy58) throws ForeignBlockchainException {
synchronized (this) {
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
walletController.initWithEntropy58(entropy58);
walletController.ensureInitialized();
walletController.ensureSynchronized();
walletController.ensureNotNullSeed();
// Get balance
String response = LiteWalletJni.execute("balance", "");
JSONObject json = new JSONObject(response);
if (json.has("zbalance")) {
return json.getLong("zbalance");
}
throw new ForeignBlockchainException("Unable to determine balance");
}
}
public List<SimpleTransaction> getWalletTransactions(String entropy58) throws ForeignBlockchainException {
synchronized (this) {
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
walletController.initWithEntropy58(entropy58);
walletController.ensureInitialized();
walletController.ensureSynchronized();
walletController.ensureNotNullSeed();
List<SimpleTransaction> transactions = new ArrayList<>();
// Get transactions list
String response = LiteWalletJni.execute("list", "");
JSONArray transactionsJson = new JSONArray(response);
if (transactionsJson != null) {
for (int i = 0; i < transactionsJson.length(); i++) {
JSONObject transactionJson = transactionsJson.getJSONObject(i);
if (transactionJson.has("txid")) {
String txId = transactionJson.getString("txid");
Long timestamp = transactionJson.getLong("datetime");
Long amount = transactionJson.getLong("amount");
Long fee = transactionJson.getLong("fee");
String memo = null;
if (transactionJson.has("incoming_metadata")) {
JSONArray incomingMetadatas = transactionJson.getJSONArray("incoming_metadata");
if (incomingMetadatas != null) {
for (int j = 0; j < incomingMetadatas.length(); j++) {
JSONObject incomingMetadata = incomingMetadatas.getJSONObject(j);
if (incomingMetadata.has("value")) {
//String address = incomingMetadata.getString("address");
Long value = incomingMetadata.getLong("value");
amount = value; // TODO: figure out how to parse transactions with multiple incomingMetadata entries
}
if (incomingMetadata.has("memo") && !incomingMetadata.isNull("memo")) {
memo = incomingMetadata.getString("memo");
}
}
}
}
if (transactionJson.has("outgoing_metadata")) {
JSONArray outgoingMetadatas = transactionJson.getJSONArray("outgoing_metadata");
for (int j = 0; j < outgoingMetadatas.length(); j++) {
JSONObject outgoingMetadata = outgoingMetadatas.getJSONObject(j);
if (outgoingMetadata.has("memo") && !outgoingMetadata.isNull("memo")) {
memo = outgoingMetadata.getString("memo");
}
}
}
long timestampMillis = Math.toIntExact(timestamp) * 1000L;
SimpleTransaction transaction = new SimpleTransaction(txId, timestampMillis, amount, fee, null, null, memo);
transactions.add(transaction);
}
}
}
return transactions;
}
}
public String getWalletAddress(String entropy58) throws ForeignBlockchainException {
synchronized (this) {
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
walletController.initWithEntropy58(entropy58);
walletController.ensureInitialized();
walletController.ensureNotNullSeed();
return walletController.getCurrentWallet().getWalletAddress();
}
}
public String getUnusedReceiveAddress(String key58) throws ForeignBlockchainException {
// For now, return the main wallet address
// FUTURE: generate an unused one
return this.getWalletAddress(key58);
}
public String sendCoins(PirateChainSendRequest pirateChainSendRequest) throws ForeignBlockchainException {
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
walletController.initWithEntropy58(pirateChainSendRequest.entropy58);
walletController.ensureInitialized();
walletController.ensureSynchronized();
walletController.ensureNotNullSeed();
// Unlock wallet
walletController.getCurrentWallet().unlock();
// Build spend
JSONObject txn = new JSONObject();
txn.put("input", walletController.getCurrentWallet().getWalletAddress());
txn.put("fee", MAINNET_FEE);
JSONObject output = new JSONObject();
output.put("address", pirateChainSendRequest.receivingAddress);
output.put("amount", pirateChainSendRequest.arrrAmount);
output.put("memo", pirateChainSendRequest.memo);
JSONArray outputs = new JSONArray();
outputs.put(output);
txn.put("output", outputs);
String txnString = txn.toString();
// Send the coins
String response = LiteWalletJni.execute("send", txnString);
JSONObject json = new JSONObject(response);
try {
if (json.has("txid")) { // Success
return json.getString("txid");
}
else if (json.has("error")) {
String error = json.getString("error");
throw new ForeignBlockchainException(error);
}
} catch (JSONException e) {
throw new ForeignBlockchainException(e.getMessage());
}
throw new ForeignBlockchainException("Something went wrong");
}
public String fundP2SH(String entropy58, String receivingAddress, long amount,
String redeemScript58) throws ForeignBlockchainException {
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
walletController.initWithEntropy58(entropy58);
walletController.ensureInitialized();
walletController.ensureSynchronized();
walletController.ensureNotNullSeed();
// Unlock wallet
walletController.getCurrentWallet().unlock();
// Build spend
JSONObject txn = new JSONObject();
txn.put("input", walletController.getCurrentWallet().getWalletAddress());
txn.put("fee", MAINNET_FEE);
JSONObject output = new JSONObject();
output.put("address", receivingAddress);
output.put("amount", amount);
//output.put("memo", memo);
JSONArray outputs = new JSONArray();
outputs.put(output);
txn.put("output", outputs);
txn.put("script", redeemScript58);
String txnString = txn.toString();
// Send the coins
String response = LiteWalletJni.execute("sendp2sh", txnString);
JSONObject json = new JSONObject(response);
try {
if (json.has("txid")) { // Success
return json.getString("txid");
}
else if (json.has("error")) {
String error = json.getString("error");
throw new ForeignBlockchainException(error);
}
} catch (JSONException e) {
throw new ForeignBlockchainException(e.getMessage());
}
throw new ForeignBlockchainException("Something went wrong");
}
public String redeemP2sh(String p2shAddress, String receivingAddress, long amount, String redeemScript58,
String fundingTxid58, String secret58, String privateKey58) throws ForeignBlockchainException {
// Use null seed wallet since we may not have the entropy bytes for a real wallet's seed
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
walletController.initNullSeedWallet();
walletController.ensureInitialized();
walletController.getCurrentWallet().unlock();
// Build spend
JSONObject txn = new JSONObject();
txn.put("input", p2shAddress);
txn.put("fee", MAINNET_FEE);
JSONObject output = new JSONObject();
output.put("address", receivingAddress);
output.put("amount", amount);
// output.put("memo", ""); // Maybe useful in future to include trade details?
JSONArray outputs = new JSONArray();
outputs.put(output);
txn.put("output", outputs);
txn.put("script", redeemScript58);
txn.put("txid", fundingTxid58);
txn.put("locktime", 0); // Must be 0 when redeeming
txn.put("secret", secret58);
txn.put("privkey", privateKey58);
String txnString = txn.toString();
// Redeem the P2SH
String response = LiteWalletJni.execute("redeemp2sh", txnString);
JSONObject json = new JSONObject(response);
try {
if (json.has("txid")) { // Success
return json.getString("txid");
}
else if (json.has("error")) {
String error = json.getString("error");
throw new ForeignBlockchainException(error);
}
} catch (JSONException e) {
throw new ForeignBlockchainException(e.getMessage());
}
throw new ForeignBlockchainException("Something went wrong");
}
public String refundP2sh(String p2shAddress, String receivingAddress, long amount, String redeemScript58,
String fundingTxid58, int lockTime, String privateKey58) throws ForeignBlockchainException {
// Use null seed wallet since we may not have the entropy bytes for a real wallet's seed
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
walletController.initNullSeedWallet();
walletController.ensureInitialized();
walletController.getCurrentWallet().unlock();
// Build spend
JSONObject txn = new JSONObject();
txn.put("input", p2shAddress);
txn.put("fee", MAINNET_FEE);
JSONObject output = new JSONObject();
output.put("address", receivingAddress);
output.put("amount", amount);
// output.put("memo", ""); // Maybe useful in future to include trade details?
JSONArray outputs = new JSONArray();
outputs.put(output);
txn.put("output", outputs);
txn.put("script", redeemScript58);
txn.put("txid", fundingTxid58);
txn.put("locktime", lockTime);
txn.put("secret", ""); // Must be blank when refunding
txn.put("privkey", privateKey58);
String txnString = txn.toString();
// Redeem the P2SH
String response = LiteWalletJni.execute("redeemp2sh", txnString);
JSONObject json = new JSONObject(response);
try {
if (json.has("txid")) { // Success
return json.getString("txid");
}
else if (json.has("error")) {
String error = json.getString("error");
throw new ForeignBlockchainException(error);
}
} catch (JSONException e) {
throw new ForeignBlockchainException(e.getMessage());
}
throw new ForeignBlockchainException("Something went wrong");
}
public String getSyncStatus(String entropy58) throws ForeignBlockchainException {
synchronized (this) {
PirateChainWalletController walletController = PirateChainWalletController.getInstance();
walletController.initWithEntropy58(entropy58);
return walletController.getSyncStatus();
}
}
public static BitcoinyTransaction deserializeRawTransaction(String rawTransactionHex) throws TransformationException {
byte[] rawTransactionData = HashCode.fromString(rawTransactionHex).asBytes();
ByteBuffer byteBuffer = ByteBuffer.wrap(rawTransactionData);
// Header
int header = BitTwiddling.readU32(byteBuffer);
boolean overwintered = ((header >> 31 & 0xff) == 255);
int version = header & 0x7FFFFFFF;
// Version group ID
int versionGroupId = 0;
if (overwintered) {
versionGroupId = BitTwiddling.readU32(byteBuffer);
}
boolean isOverwinterV3 = overwintered && versionGroupId == 0x03C48270 && version == 3;
boolean isSaplingV4 = overwintered && versionGroupId == 0x892F2085 && version == 4;
if (overwintered && !(isOverwinterV3 || isSaplingV4)) {
throw new TransformationException("Unknown transaction format");
}
// Inputs
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
int vinCount = BitTwiddling.readU8(byteBuffer);
for (int i=0; i<vinCount; i++) {
// Outpoint hash
byte[] outpointHashBytes = new byte[32];
byteBuffer.get(outpointHashBytes);
String outpointHash = HashCode.fromBytes(outpointHashBytes).toString();
// vout
int vout = BitTwiddling.readU32(byteBuffer);
// scriptSig
int scriptSigLength = BitTwiddling.readU8(byteBuffer);
byte[] scriptSigBytes = new byte[scriptSigLength];
byteBuffer.get(scriptSigBytes);
String scriptSig = HashCode.fromBytes(scriptSigBytes).toString();
int sequence = BitTwiddling.readU32(byteBuffer);
BitcoinyTransaction.Input input = new BitcoinyTransaction.Input(scriptSig, sequence, outpointHash, vout);
inputs.add(input);
}
// Outputs
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
int voutCount = BitTwiddling.readU8(byteBuffer);
for (int i=0; i<voutCount; i++) {
// Amount
byte[] amountBytes = new byte[8];
byteBuffer.get(amountBytes);
long amount = BitTwiddling.longFromLEBytes(amountBytes, 0);
// Script pubkey
int scriptPubkeySize = BitTwiddling.readU8(byteBuffer);
byte[] scriptPubkeyBytes = new byte[scriptPubkeySize];
byteBuffer.get(scriptPubkeyBytes);
String scriptPubKey = HashCode.fromBytes(scriptPubkeyBytes).toString();
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, amount, null));
}
// Locktime
byte[] locktimeBytes = new byte[4];
byteBuffer.get(locktimeBytes);
int locktime = BitTwiddling.intFromLEBytes(locktimeBytes, 0);
// Expiry height
int expiryHeight = 0;
if (isOverwinterV3 || isSaplingV4) {
byte[] expiryHeightBytes = new byte[4];
byteBuffer.get(expiryHeightBytes);
expiryHeight = BitTwiddling.intFromLEBytes(expiryHeightBytes, 0);
}
String txHash = null; // Not present in raw transaction data
int size = 0; // Not present in raw transaction data
Integer timestamp = null; // Not present in raw transaction data
// Note: this is incomplete, as sapling spend info is not yet parsed. We don't need it for our
// current trade bot implementation, but it could be added in the future, for completeness.
// See link below for reference:
// https://github.com/PirateNetwork/librustzcash/blob/2981c4d2860f7cd73282fed885daac0323ff0280/zcash_primitives/src/transaction/mod.rs#L197
return new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
}
}

View File

@@ -2,8 +2,6 @@ package org.qortal.crosschain;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.*;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
@@ -29,7 +27,7 @@ import static org.ciyam.at.OpCode.calcOffset;
*
* <p>
* <ul>
* <li>Bob generates Dogecoin & Qortal 'trade' keys
* <li>Bob generates PirateChain & Qortal 'trade' keys
* <ul>
* <li>private key required to sign P2SH redeem tx</li>
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li>
@@ -42,12 +40,12 @@ import static org.ciyam.at.OpCode.calcOffset;
* </li>
* <li>Alice finds Qortal AT and wants to trade
* <ul>
* <li>Alice generates Dogecoin & Qortal 'trade' keys</li>
* <li>Alice funds Dogecoin P2SH-A</li>
* <li>Alice generates PirateChain & Qortal 'trade' keys</li>
* <li>Alice funds PirateChain P2SH-A</li>
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
* <ul>
* <li>hash-of-secret-A</li>
* <li>her 'trade' Dogecoin PKH</li>
* <li>her 'trade' Pirate Chain public key</li>
* </ul>
* </li>
* </ul>
@@ -58,7 +56,7 @@ import static org.ciyam.at.OpCode.calcOffset;
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
* <ul>
* <li>Alice's trade Qortal address</li>
* <li>Alice's trade Dogecoin PKH</li>
* <li>Alice's trade Pirate Chain public key</li>
* <li>hash-of-secret-A</li>
* </ul>
* </li>
@@ -77,48 +75,46 @@ import static org.ciyam.at.OpCode.calcOffset;
* </li>
* <li>Bob checks AT, extracts secret-A
* <ul>
* <li>Bob redeems P2SH-A using his Dogecoin trade key and secret-A</li>
* <li>P2SH-A DOGE funds end up at Dogecoin address determined by redeem transaction output(s)</li>
* <li>Bob redeems P2SH-A using his PirateChain trade key and secret-A</li>
* <li>P2SH-A ARRR funds end up at PirateChain address determined by redeem transaction output(s)</li>
* </ul>
* </li>
* </ul>
*/
public class DogecoinACCTv2 implements ACCT {
public class PirateChainACCTv3 implements ACCT {
private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2.class);
public static final String NAME = DogecoinACCTv2.class.getSimpleName();
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("6fff38d6eeb06568a9c879c5628527730319844aa0de53f5f4ffab5506efe885").asBytes(); // SHA256 of AT code bytes
public static final String NAME = PirateChainACCTv3.class.getSimpleName();
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("fc2818ac0819ab658a065ab0d050e75f167921e2dce5969b9b7741e47e477d83").asBytes(); // SHA256 of AT code bytes
public static final int SECRET_LENGTH = 32;
/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
private static final int MODE_VALUE_OFFSET = 61;
private static final int MODE_VALUE_OFFSET = 68;
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
public static class OfferMessageData {
public byte[] partnerDogecoinPKH;
public byte[] partnerPirateChainPublicKey;
public byte[] hashOfSecretA;
public long lockTimeA;
}
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDogecoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
public static final int OFFER_MESSAGE_LENGTH = 33 /*partnerPirateChainPublicKey*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
+ 24 /*partner's Dogecoin PKH (padded from 20 to 24)*/
+ 40 /*partner's Pirate Chain public key (padded from 33 to 40)*/
+ 8 /*AT trade timeout (minutes)*/
+ 24 /*hash of secret-A (padded from 20 to 24)*/
+ 8 /*lockTimeA*/;
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
private static DogecoinACCTv2 instance;
private static PirateChainACCTv3 instance;
private DogecoinACCTv2() {
private PirateChainACCTv3() {
}
public static synchronized DogecoinACCTv2 getInstance() {
public static synchronized PirateChainACCTv3 getInstance() {
if (instance == null)
instance = new DogecoinACCTv2();
instance = new PirateChainACCTv3();
return instance;
}
@@ -135,7 +131,7 @@ public class DogecoinACCTv2 implements ACCT {
@Override
public ForeignBlockchain getBlockchain() {
return Dogecoin.getInstance();
return PirateChain.getInstance();
}
/**
@@ -145,14 +141,14 @@ public class DogecoinACCTv2 implements ACCT {
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
*
* @param creatorTradeAddress AT creator's trade Qortal address
* @param dogecoinPublicKeyHash 20-byte HASH160 of creator's trade Dogecoin public key
* @param pirateChainPublicKeyHash 33-byte creator's trade PirateChain public key
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
* @param dogecoinAmount how much DOGE the AT creator is expecting to trade
* @param arrrAmount how much ARRR the AT creator is expecting to trade
* @param tradeTimeout suggested timeout for entire trade
*/
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] dogecoinPublicKeyHash, long qortAmount, long dogecoinAmount, int tradeTimeout) {
if (dogecoinPublicKeyHash.length != 20)
throw new IllegalArgumentException("Dogecoin public key hash should be 20 bytes");
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] pirateChainPublicKeyHash, long qortAmount, long arrrAmount, int tradeTimeout) {
if (pirateChainPublicKeyHash.length != 33)
throw new IllegalArgumentException("PirateChain public key hash should be 33 bytes");
// Labels for data segment addresses
int addrCounter = 0;
@@ -164,11 +160,11 @@ public class DogecoinACCTv2 implements ACCT {
final int addrCreatorTradeAddress3 = addrCounter++;
final int addrCreatorTradeAddress4 = addrCounter++;
final int addrDogecoinPublicKeyHash = addrCounter;
addrCounter += 4;
final int addrPirateChainPublicKeyHash = addrCounter;
addrCounter += 5;
final int addrQortAmount = addrCounter++;
final int addrDogecoinAmount = addrCounter++;
final int addrarrrAmount = addrCounter++;
final int addrTradeTimeout = addrCounter++;
final int addrMessageTxnType = addrCounter++;
@@ -179,8 +175,10 @@ public class DogecoinACCTv2 implements ACCT {
final int addrQortalPartnerAddressPointer = addrCounter++;
final int addrMessageSenderPointer = addrCounter++;
final int addrTradeMessagePartnerDogecoinPKHOffset = addrCounter++;
final int addrPartnerDogecoinPKHPointer = addrCounter++;
final int addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset = addrCounter++;
final int addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset = addrCounter++; // Remainder of public key, plus timeout
final int addrPartnerPirateChainPublicKeyFirst32BytesPointer = addrCounter++;
final int addrPartnerPirateChainPublicKeyLastBytePointer = addrCounter++; // Remainder of public key
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
final int addrHashOfSecretAPointer = addrCounter++;
@@ -226,9 +224,12 @@ public class DogecoinACCTv2 implements ACCT {
final int addrHashOfSecretA = addrCounter;
addrCounter += 4;
final int addrPartnerDogecoinPKH = addrCounter;
final int addrPartnerPirateChainPublicKeyFirst32Bytes = addrCounter;
addrCounter += 4;
final int addrPartnerPirateChainPublicKeyLastByte = addrCounter;
addrCounter += 4; // We retrieve using GET_B_IND, so need to allow space for the full 32 bytes
final int addrPartnerReceivingAddress = addrCounter;
addrCounter += 4;
@@ -243,17 +244,17 @@ public class DogecoinACCTv2 implements ACCT {
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
// Dogecoin public key hash
assert dataByteBuffer.position() == addrDogecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrDogecoinPublicKeyHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(dogecoinPublicKeyHash, 32, 0));
// PirateChain public key hash
assert dataByteBuffer.position() == addrPirateChainPublicKeyHash * MachineState.VALUE_SIZE : "addrPirateChainPublicKeyHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(pirateChainPublicKeyHash, 40, 0));
// Redeem Qort amount
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
dataByteBuffer.putLong(qortAmount);
// Expected Dogecoin amount
assert dataByteBuffer.position() == addrDogecoinAmount * MachineState.VALUE_SIZE : "addrDogecoinAmount incorrect";
dataByteBuffer.putLong(dogecoinAmount);
// Expected PirateChain amount
assert dataByteBuffer.position() == addrarrrAmount * MachineState.VALUE_SIZE : "addrarrrAmount incorrect";
dataByteBuffer.putLong(arrrAmount);
// Suggested trade timeout (minutes)
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
@@ -283,17 +284,25 @@ public class DogecoinACCTv2 implements ACCT {
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
dataByteBuffer.putLong(addrMessageSender1);
// Offset into 'trade' MESSAGE data payload for extracting partner's Dogecoin PKH
assert dataByteBuffer.position() == addrTradeMessagePartnerDogecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDogecoinPKHOffset incorrect";
// Offset into 'trade' MESSAGE data payload for extracting first 32 bytes of partner's Pirate Chain public key
assert dataByteBuffer.position() == addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset incorrect";
dataByteBuffer.putLong(32L);
// Index into data segment of partner's Dogecoin PKH, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerDogecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerDogecoinPKHPointer incorrect";
dataByteBuffer.putLong(addrPartnerDogecoinPKH);
// Offset into 'trade' MESSAGE data payload for extracting last byte of public key
assert dataByteBuffer.position() == addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset incorrect";
dataByteBuffer.putLong(64L);
// Index into data segment of partner's Pirate Chain public key, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerPirateChainPublicKeyFirst32BytesPointer * MachineState.VALUE_SIZE : "addrPartnerPirateChainPublicKeyFirst32BytesPointer incorrect";
dataByteBuffer.putLong(addrPartnerPirateChainPublicKeyFirst32Bytes);
// Index into data segment of remainder of partner's Pirate Chain public key, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerPirateChainPublicKeyLastBytePointer * MachineState.VALUE_SIZE : "addrPartnerPirateChainPublicKeyLastBytePointer incorrect";
dataByteBuffer.putLong(addrPartnerPirateChainPublicKeyLastByte);
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
dataByteBuffer.putLong(64L);
dataByteBuffer.putLong(80L);
// Index into data segment to hash of secret A, used by GET_B_IND
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
@@ -338,9 +347,6 @@ public class DogecoinACCTv2 implements ACCT {
try {
/* Initialization */
/* NOP - to ensure DOGECOIN ACCT is unique */
codeByteBuffer.put(OpCode.NOP.compile());
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
@@ -429,12 +435,17 @@ public class DogecoinACCTv2 implements ACCT {
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
// Extract trade partner's Dogecoin public key hash (PKH) from message into B
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDogecoinPKHOffset));
// Store partner's Dogecoin PKH (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDogecoinPKHPointer));
// Extract AT trade timeout (minutes) (from B4)
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
// Extract first 32 bytes of trade partner's Pirate Chain public key from message into B
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerPirateChainPublicKeyFirst32BytesOffset));
// Store first 32 bytes of partner's Pirate Chain public key
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerPirateChainPublicKeyFirst32BytesPointer));
// Extract last byte of public key, plus trade timeout, from message into B
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerPirateChainPublicKeyLastByteOffset));
// Store last byte of partner's Pirate Chain public key
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerPirateChainPublicKeyLastBytePointer));
// Extract AT trade timeout (minutes) (from B2)
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B2, addrRefundTimeout));
// Grab next 32 bytes
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
@@ -465,9 +476,6 @@ public class DogecoinACCTv2 implements ACCT {
/* Transaction processing loop */
labelRedeemTxnLoop = codeByteBuffer.position();
/* Sleep until message arrives */
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
@@ -546,7 +554,7 @@ public class DogecoinACCTv2 implements ACCT {
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile DOGE-QORT ACCT?", e);
throw new IllegalStateException("Unable to compile ARRR-QORT ACCT?", e);
}
}
@@ -555,7 +563,7 @@ public class DogecoinACCTv2 implements ACCT {
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
assert Arrays.equals(Crypto.digest(codeBytes), DogecoinACCTv2.CODE_BYTES_HASH)
assert Arrays.equals(Crypto.digest(codeBytes), PirateChainACCTv3.CODE_BYTES_HASH)
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
final short ciyamAtVersion = 2;
@@ -593,7 +601,7 @@ public class DogecoinACCTv2 implements ACCT {
CrossChainTradeData tradeData = new CrossChainTradeData();
tradeData.foreignBlockchain = SupportedBlockchain.DOGECOIN.name();
tradeData.foreignBlockchain = SupportedBlockchain.PIRATECHAIN.name();
tradeData.acctName = NAME;
tradeData.qortalAtAddress = atAddress;
@@ -614,10 +622,10 @@ public class DogecoinACCTv2 implements ACCT {
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
// Creator's Dogecoin/foreign public key hash
tradeData.creatorForeignPKH = new byte[20];
// Creator's PirateChain/foreign public key (full 33 bytes, not hashed, so ignore references to "PKH")
tradeData.creatorForeignPKH = new byte[33];
dataByteBuffer.get(tradeData.creatorForeignPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
dataByteBuffer.position(dataByteBuffer.position() + 40 - tradeData.creatorForeignPKH.length); // skip to 40 bytes
// We don't use secret-B
tradeData.hashOfSecretB = null;
@@ -625,7 +633,7 @@ public class DogecoinACCTv2 implements ACCT {
// Redeem payout
tradeData.qortAmount = dataByteBuffer.getLong();
// Expected DOGE amount
// Expected ARRR amount
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
// Trade timeout
@@ -649,10 +657,16 @@ public class DogecoinACCTv2 implements ACCT {
// Skip pointer to message sender
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for partner's Dogecoin PKH
// Skip 'trade' message data offset for first 32 bytes of partner's Pirate Chain public key
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Dogecoin PKH
// Skip 'trade' message data offset for last 32 byte of partner's Pirate Chain public key
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Pirate Chain public key (first 32 bytes)
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Pirate Chain public key (last byte)
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for hash-of-secret-A
@@ -718,10 +732,10 @@ public class DogecoinACCTv2 implements ACCT {
dataByteBuffer.get(hashOfSecretA);
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
// Potential partner's Dogecoin PKH
byte[] partnerDogecoinPKH = new byte[20];
dataByteBuffer.get(partnerDogecoinPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDogecoinPKH.length); // skip to 32 bytes
// Potential partner's PirateChain public key
byte[] partnerPirateChainPublicKey = new byte[33];
dataByteBuffer.get(partnerPirateChainPublicKey);
dataByteBuffer.position(dataByteBuffer.position() + 64 - partnerPirateChainPublicKey.length); // skip to 64 bytes
// Partner's receiving address (if present)
byte[] partnerReceivingAddress = new byte[25];
@@ -740,7 +754,7 @@ public class DogecoinACCTv2 implements ACCT {
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
tradeData.qortalPartnerAddress = qortalRecipient;
tradeData.hashOfSecretA = hashOfSecretA;
tradeData.partnerForeignPKH = partnerDogecoinPKH;
tradeData.partnerForeignPKH = partnerPirateChainPublicKey; // Not hashed
tradeData.lockTimeA = lockTimeA;
if (mode == AcctMode.REDEEMED)
@@ -755,9 +769,9 @@ public class DogecoinACCTv2 implements ACCT {
}
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
public static byte[] buildOfferMessage(byte[] partnerBitcoinPublicKey, byte[] hashOfSecretA, int lockTimeA) {
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
return Bytes.concat(partnerBitcoinPublicKey, hashOfSecretA, lockTimeABytes);
}
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
@@ -766,25 +780,25 @@ public class DogecoinACCTv2 implements ACCT {
return null;
OfferMessageData offerMessageData = new OfferMessageData();
offerMessageData.partnerDogecoinPKH = Arrays.copyOfRange(messageData, 0, 20);
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
offerMessageData.partnerPirateChainPublicKey = Arrays.copyOfRange(messageData, 0, 33);
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 33, 53);
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 53);
return offerMessageData;
}
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPublicKey, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
byte[] data = new byte[TRADE_MESSAGE_LENGTH];
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
System.arraycopy(partnerBitcoinPublicKey, 0, data, 32, partnerBitcoinPublicKey.length);
System.arraycopy(refundTimeoutBytes, 0, data, 72, refundTimeoutBytes.length);
System.arraycopy(hashOfSecretA, 0, data, 80, hashOfSecretA.length);
System.arraycopy(lockTimeABytes, 0, data, 104, lockTimeABytes.length);
return data;
}

View File

@@ -0,0 +1,404 @@
package org.qortal.crosschain;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptChunk;
import org.qortal.crypto.Crypto;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import java.util.*;
import static org.qortal.crosschain.BitcoinyHTLC.Status;
public class PirateChainHTLC {
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
// Assuming node's trade-bot has no more than 100 entries?
private static final int MAX_CACHE_ENTRIES = 100;
// Max time-to-live for cache entries (milliseconds)
private static final long CACHE_TIMEOUT = 30_000L;
@SuppressWarnings("serial")
private static final Map<String, byte[]> SECRET_CACHE = new LinkedHashMap<>(MAX_CACHE_ENTRIES + 1, 0.75F, true) {
// This method is called just after a new entry has been added
@Override
public boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
return size() > MAX_CACHE_ENTRIES;
}
};
private static final byte[] NO_SECRET_CACHE_ENTRY = new byte[0];
@SuppressWarnings("serial")
private static final Map<String, Status> STATUS_CACHE = new LinkedHashMap<>(MAX_CACHE_ENTRIES + 1, 0.75F, true) {
// This method is called just after a new entry has been added
@Override
public boolean removeEldestEntry(Map.Entry<String, Status> eldest) {
return size() > MAX_CACHE_ENTRIES;
}
};
/*
* OP_RETURN + OP_PUSHDATA1 + bytes (not part of actual redeem script - used for "push only" secondary output when funding P2SH)
*
* OP_IF (if top stack value isn't false) (true=refund; false=redeem) (boolean is then removed from stack)
* <push 4 bytes> <intended locktime>
* OP_CHECKLOCKTIMEVERIFY (if stack locktime greater than transaction's lock time - i.e. refunding but too soon - then fail validation)
* OP_DROP (remove locktime from top of stack)
* <push 33 bytes> <intended refunder public key>
* OP_CHECKSIG (check signature and public key are correct; returns 1 or 0)
* OP_ELSE (if top stack value was false, i.e. attempting to redeem)
* OP_SIZE (push length of top item - the secret - to the top of the stack)
* <push 1 byte> 32
* OP_EQUALVERIFY (unhashed secret must be 32 bytes in length)
* OP_HASH160 (hash the secret)
* <push 20 bytes> <intended secret hash>
* OP_EQUALVERIFY (ensure hash of supplied secret matches intended secret hash; transaction invalid if no match)
* <push 33 bytes> <intended redeemer public key>
* OP_CHECKSIG (check signature and public key are correct; returns 1 or 0)
* OP_ENDIF
*/
private static final byte[] pushOnlyPrefix = HashCode.fromString("6a4c").asBytes(); // OP_RETURN + push(redeem script)
private static final byte[] redeemScript1 = HashCode.fromString("6304").asBytes(); // OP_IF push(4 bytes locktime)
private static final byte[] redeemScript2 = HashCode.fromString("b17521").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_DROP push(33 bytes refund pubkey)
private static final byte[] redeemScript3 = HashCode.fromString("ac6782012088a914").asBytes(); // OP_CHECKSIG OP_ELSE OP_SIZE push(0x20) OP_EQUALVERIFY OP_HASH160 push(20 bytes hash of secret)
private static final byte[] redeemScript4 = HashCode.fromString("8821").asBytes(); // OP_EQUALVERIFY push(33 bytes redeem pubkey)
private static final byte[] redeemScript5 = HashCode.fromString("ac68").asBytes(); // OP_CHECKSIG OP_ENDIF
/**
* Returns redeemScript used for cross-chain trading.
* <p>
* See comments in {@link PirateChainHTLC} for more details.
*
* @param refunderPubKey 33-byte P2SH funder's public key, for refunding purposes
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
* @param redeemerPubKey 33-byte P2SH redeemer's public key
* @param hashOfSecret 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
*/
public static byte[] buildScript(byte[] refunderPubKey, int lockTime, byte[] redeemerPubKey, byte[] hashOfSecret) {
return Bytes.concat(redeemScript1, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), redeemScript2,
refunderPubKey, redeemScript3, hashOfSecret, redeemScript4, redeemerPubKey, redeemScript5);
}
/**
* Alternative to buildScript() above, this time with a prefix suitable for adding the redeem script
* to a "push only" output (via OP_RETURN followed by OP_PUSHDATA1)
*
* @param refunderPubKey 33-byte P2SH funder's public key, for refunding purposes
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
* @param redeemerPubKey 33-byte P2SH redeemer's public key
* @param hashOfSecret 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
* @return
*/
public static byte[] buildScriptWithPrefix(byte[] refunderPubKey, int lockTime, byte[] redeemerPubKey, byte[] hashOfSecret) {
byte[] redeemScript = buildScript(refunderPubKey, lockTime, redeemerPubKey, hashOfSecret);
int size = redeemScript.length;
String sizeHex = Integer.toHexString(size & 0xFF);
return Bytes.concat(pushOnlyPrefix, HashCode.fromString(sizeHex).asBytes(), redeemScript);
}
/**
* Returns 'secret', if any, given HTLC's P2SH address.
* <p>
* @throws ForeignBlockchainException
*/
public static byte[] findHtlcSecret(Bitcoiny bitcoiny, String p2shAddress) throws ForeignBlockchainException {
NetworkParameters params = bitcoiny.getNetworkParameters();
String compoundKey = String.format("%s-%s-%d", params.getId(), p2shAddress, System.currentTimeMillis() / CACHE_TIMEOUT);
byte[] secret = SECRET_CACHE.getOrDefault(compoundKey, NO_SECRET_CACHE_ENTRY);
if (secret != NO_SECRET_CACHE_ENTRY)
return secret;
List<byte[]> rawTransactions = bitcoiny.getAddressTransactions(p2shAddress);
for (byte[] rawTransaction : rawTransactions) {
Transaction transaction = new Transaction(params, rawTransaction);
// Cycle through inputs, looking for one that spends our HTLC
for (TransactionInput input : transaction.getInputs()) {
Script scriptSig = input.getScriptSig();
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
// Expected number of script chunks for redeem. Refund might not have the same number.
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
if (scriptChunks.size() != expectedChunkCount)
continue;
// We're expecting last chunk to contain the actual redeemScript
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
byte[] redeemScriptBytes = lastChunk.data;
// If non-push scripts, redeemScript will be null
if (redeemScriptBytes == null)
continue;
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!inputAddress.toString().equals(p2shAddress))
// Input isn't spending our HTLC
continue;
secret = scriptChunks.get(0).data;
if (secret.length != PirateChainHTLC.SECRET_LENGTH)
continue;
// Cache secret for a while
SECRET_CACHE.put(compoundKey, secret);
return secret;
}
}
// Cache negative result
SECRET_CACHE.put(compoundKey, null);
return null;
}
/**
* Returns a string containing the txid of the transaction that funded supplied <tt>p2shAddress</tt>
* We have to do this in a bit of a roundabout way due to the Pirate Light Client server omitting
* transaction hashes from the raw transaction data.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
public static String getFundingTxid(BitcoinyBlockchainProvider blockchain, String p2shAddress) throws ForeignBlockchainException {
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
// HASH160(redeem script) for this p2shAddress
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
// Firstly look for an unspent output
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
List<UnspentOutput> unspentOutputs = blockchain.getUnspentOutputs(p2shAddress, false);
for (UnspentOutput unspentOutput : unspentOutputs) {
if (!Arrays.equals(ourScriptPubKey, unspentOutput.script)) {
continue;
}
return HashCode.fromBytes(unspentOutput.hash).toString();
}
// No valid unspent outputs, so must be already spent...
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
List<BitcoinyTransaction> transactions = blockchain.getAddressBitcoinyTransactions(p2shAddress, BitcoinyBlockchainProvider.EXCLUDE_UNCONFIRMED);
// Sort by confirmed first, followed by ascending height
transactions.sort(BitcoinyTransaction.CONFIRMED_FIRST.thenComparing(BitcoinyTransaction::getHeight));
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
// Acceptable funding is one transaction output, so we're expecting only one input
if (bitcoinyTransaction.inputs.size() != 1)
// Wrong number of inputs
continue;
String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
// Not valid chunks for our form of HTLC
continue;
// Last chunk is redeem script
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
// Not spending our specific HTLC redeem script
continue;
return bitcoinyTransaction.inputs.get(0).outputTxHash;
}
return null;
}
/**
* Returns a string containing the unspent txid of the transaction that funded supplied <tt>p2shAddress</tt>
* and is at least the value specified in <tt>minimumAmount</tt>
* <p>
* @throws ForeignBlockchainException if error occurs
*/
public static String getUnspentFundingTxid(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException {
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
List<UnspentOutput> unspentOutputs = blockchain.getUnspentOutputs(p2shAddress, false);
for (UnspentOutput unspentOutput : unspentOutputs) {
if (!Arrays.equals(ourScriptPubKey, unspentOutput.script)) {
// Not funding our specific HTLC script hash
continue;
}
if (unspentOutput.value < minimumAmount) {
// Not funding the required amount
continue;
}
return HashCode.fromBytes(unspentOutput.hash).toString();
}
// No valid unspent outputs, so must be already spent
return null;
}
/**
* Returns HTLC status, given P2SH address and expected redeem/refund amount
* <p>
* @throws ForeignBlockchainException if error occurs
*/
public static Status determineHtlcStatus(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException {
String compoundKey = String.format("%s-%s-%d", blockchain.getNetId(), p2shAddress, System.currentTimeMillis() / CACHE_TIMEOUT);
Status cachedStatus = STATUS_CACHE.getOrDefault(compoundKey, null);
if (cachedStatus != null)
return cachedStatus;
byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
// Note: we can't include unconfirmed transactions here because the Pirate light wallet server requires a block range
List<BitcoinyTransaction> transactions = blockchain.getAddressBitcoinyTransactions(p2shAddress, BitcoinyBlockchainProvider.EXCLUDE_UNCONFIRMED);
// Sort by confirmed first, followed by ascending height
transactions.sort(BitcoinyTransaction.CONFIRMED_FIRST.thenComparing(BitcoinyTransaction::getHeight));
// Transaction cache
//Map<String, BitcoinyTransaction> transactionsByHash = new HashMap<>();
// HASH160(redeem script) for this p2shAddress
byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
// Check for spends first, caching full transaction info as we progress just in case we don't return in this loop
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
// Cache for possible later reuse
// transactionsByHash.put(transactionInfo.txHash, bitcoinyTransaction);
// Acceptable funding is one transaction output, so we're expecting only one input
if (bitcoinyTransaction.inputs.size() != 1)
// Wrong number of inputs
continue;
String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
List<byte[]> scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
// Not valid chunks for our form of HTLC
continue;
// Last chunk is redeem script
byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
// Not spending our specific HTLC redeem script
continue;
if (scriptSigChunks.size() == 4)
// If we have 4 chunks, then secret is present, hence redeem
cachedStatus = bitcoinyTransaction.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED;
else
cachedStatus = bitcoinyTransaction.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED;
STATUS_CACHE.put(compoundKey, cachedStatus);
return cachedStatus;
}
String ourScriptPubKeyHex = HashCode.fromBytes(ourScriptPubKey).toString();
// Check for funding
for (BitcoinyTransaction bitcoinyTransaction : transactions) {
if (bitcoinyTransaction == null)
// Should be present in map!
throw new ForeignBlockchainException("Cached Bitcoin transaction now missing?");
// Check outputs for our specific P2SH
for (BitcoinyTransaction.Output output : bitcoinyTransaction.outputs) {
// Check amount
if (output.value < minimumAmount)
// Output amount too small (not taking fees into account)
continue;
String scriptPubKeyHex = output.scriptPubKey;
if (!scriptPubKeyHex.equals(ourScriptPubKeyHex))
// Not funding our specific P2SH
continue;
cachedStatus = bitcoinyTransaction.height == 0 ? Status.FUNDING_IN_PROGRESS : Status.FUNDED;
STATUS_CACHE.put(compoundKey, cachedStatus);
return cachedStatus;
}
}
cachedStatus = Status.UNFUNDED;
STATUS_CACHE.put(compoundKey, cachedStatus);
return cachedStatus;
}
private static List<byte[]> extractScriptSigChunks(byte[] scriptSigBytes) {
List<byte[]> chunks = new ArrayList<>();
int offset = 0;
int previousOffset = 0;
while (offset < scriptSigBytes.length) {
byte pushOp = scriptSigBytes[offset++];
if (pushOp < 0 || pushOp > 0x4c)
// Unacceptable OP
return Collections.emptyList();
// Special treatment for OP_PUSHDATA1
if (pushOp == 0x4c) {
if (offset >= scriptSigBytes.length)
// Run out of scriptSig bytes?
return Collections.emptyList();
pushOp = scriptSigBytes[offset++];
}
previousOffset = offset;
offset += Byte.toUnsignedInt(pushOp);
byte[] chunk = Arrays.copyOfRange(scriptSigBytes, previousOffset, offset);
chunks.add(chunk);
}
return chunks;
}
private static byte[] addressToScriptPubKey(String p2shAddress) {
// We want the HASH160 part of the P2SH address
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
byte[] scriptPubKey = new byte[1 + 1 + 20 + 1];
scriptPubKey[0x00] = (byte) 0xa9; /* OP_HASH160 */
scriptPubKey[0x01] = (byte) 0x14; /* PUSH 0x14 bytes */
System.arraycopy(p2shAddressBytes, 1, scriptPubKey, 2, 0x14);
scriptPubKey[0x16] = (byte) 0x87; /* OP_EQUAL */
return scriptPubKey;
}
private static byte[] addressToRedeemScriptHash(String p2shAddress) {
// We want the HASH160 part of the P2SH address
byte[] p2shAddressBytes = Base58.decode(p2shAddress);
return Arrays.copyOfRange(p2shAddressBytes, 1, 1 + 20);
}
}

View File

@@ -0,0 +1,649 @@
package org.qortal.crosschain;
import cash.z.wallet.sdk.rpc.CompactFormats.*;
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc;
import cash.z.wallet.sdk.rpc.Service;
import cash.z.wallet.sdk.rpc.Service.*;
import com.google.common.hash.HashCode;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.qortal.settings.Settings;
import org.qortal.transform.TransformationException;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
/** Pirate Chain network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
public class PirateLightClient extends BitcoinyBlockchainProvider {
private static final Logger LOGGER = LogManager.getLogger(PirateLightClient.class);
private static final Random RANDOM = new Random();
private static final int RESPONSE_TIME_READINGS = 5;
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
public static class Server {
String hostname;
public enum ConnectionType { TCP, SSL }
ConnectionType connectionType;
int port;
private List<Long> responseTimes = new ArrayList<>();
public Server(String hostname, ConnectionType connectionType, int port) {
this.hostname = hostname;
this.connectionType = connectionType;
this.port = port;
}
public void addResponseTime(long responseTime) {
while (this.responseTimes.size() > RESPONSE_TIME_READINGS) {
this.responseTimes.remove(0);
}
this.responseTimes.add(responseTime);
}
public long averageResponseTime() {
if (this.responseTimes.size() < RESPONSE_TIME_READINGS) {
// Not enough readings yet
return 0L;
}
OptionalDouble average = this.responseTimes.stream().mapToDouble(a -> a).average();
if (average.isPresent()) {
return Double.valueOf(average.getAsDouble()).longValue();
}
return 0L;
}
@Override
public boolean equals(Object other) {
if (other == this)
return true;
if (!(other instanceof Server))
return false;
Server otherServer = (Server) other;
return this.connectionType == otherServer.connectionType
&& this.port == otherServer.port
&& this.hostname.equals(otherServer.hostname);
}
@Override
public int hashCode() {
return this.hostname.hashCode() ^ this.port;
}
@Override
public String toString() {
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
}
}
private Set<Server> servers = new HashSet<>();
private List<Server> remainingServers = new ArrayList<>();
private Set<Server> uselessServers = Collections.synchronizedSet(new HashSet<>());
private final String netId;
private final String expectedGenesisHash;
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
private Bitcoiny blockchain;
private final Object serverLock = new Object();
private Server currentServer;
private ManagedChannel channel;
private int nextId = 1;
private static final int TX_CACHE_SIZE = 1000;
@SuppressWarnings("serial")
private final Map<String, BitcoinyTransaction> transactionCache = Collections.synchronizedMap(new LinkedHashMap<>(TX_CACHE_SIZE + 1, 0.75F, true) {
// This method is called just after a new entry has been added
@Override
public boolean removeEldestEntry(Map.Entry<String, BitcoinyTransaction> eldest) {
return size() > TX_CACHE_SIZE;
}
});
// Constructors
public PirateLightClient(String netId, String genesisHash, Collection<Server> initialServerList, Map<Server.ConnectionType, Integer> defaultPorts) {
this.netId = netId;
this.expectedGenesisHash = genesisHash;
this.servers.addAll(initialServerList);
this.defaultPorts.putAll(defaultPorts);
}
// Methods for use by other classes
@Override
public void setBlockchain(Bitcoiny blockchain) {
this.blockchain = blockchain;
}
@Override
public String getNetId() {
return this.netId;
}
/**
* Returns current blockchain height.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public int getCurrentHeight() throws ForeignBlockchainException {
BlockID latestBlock = this.getCompactTxStreamerStub().getLatestBlock(null);
if (!(latestBlock instanceof BlockID))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getLatestBlock gRPC");
return (int)latestBlock.getHeight();
}
/**
* Returns list of compact blocks, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
* @return
*/
@Override
public List<CompactBlock> getCompactBlocks(int startHeight, int count) throws ForeignBlockchainException {
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
Iterator<CompactBlock> blocksIterator = this.getCompactTxStreamerStub().getBlockRange(range);
// Map from Iterator to List
List<CompactBlock> blocks = new ArrayList<>();
blocksIterator.forEachRemaining(blocks::add);
return blocks;
}
/**
* Returns list of raw block headers, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<byte[]> getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException {
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
Iterator<CompactBlock> blocks = this.getCompactTxStreamerStub().getBlockRange(range);
List<byte[]> rawBlockHeaders = new ArrayList<>();
while (blocks.hasNext()) {
CompactBlock block = blocks.next();
if (block.getHeader() == null) {
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getBlockRange gRPC");
}
rawBlockHeaders.add(block.getHeader().toByteArray());
}
return rawBlockHeaders;
}
/**
* Returns list of raw block timestamps, starting from <tt>startHeight</tt> inclusive.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<Long> getBlockTimestamps(int startHeight, int count) throws ForeignBlockchainException {
BlockID startBlock = BlockID.newBuilder().setHeight(startHeight).build();
BlockID endBlock = BlockID.newBuilder().setHeight(startHeight + count - 1).build();
BlockRange range = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
Iterator<CompactBlock> blocks = this.getCompactTxStreamerStub().getBlockRange(range);
List<Long> rawBlockTimestamps = new ArrayList<>();
while (blocks.hasNext()) {
CompactBlock block = blocks.next();
if (block.getTime() <= 0) {
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getBlockRange gRPC");
}
rawBlockTimestamps.add(Long.valueOf(block.getTime()));
}
return rawBlockTimestamps;
}
/**
* Returns confirmed balance, based on passed payment script.
* <p>
* @return confirmed balance, or zero if script unknown
* @throws ForeignBlockchainException if there was an error
*/
@Override
public long getConfirmedBalance(byte[] script) throws ForeignBlockchainException {
throw new ForeignBlockchainException("getConfirmedBalance not yet implemented for Pirate Chain");
}
/**
* Returns confirmed balance, based on passed base58 encoded address.
* <p>
* @return confirmed balance, or zero if address unknown
* @throws ForeignBlockchainException if there was an error
*/
@Override
public long getConfirmedAddressBalance(String base58Address) throws ForeignBlockchainException {
AddressList addressList = AddressList.newBuilder().addAddresses(base58Address).build();
Balance balance = this.getCompactTxStreamerStub().getTaddressBalance(addressList);
if (!(balance instanceof Balance))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getConfirmedAddressBalance gRPC");
return balance.getValueZat();
}
/**
* Returns list of unspent outputs pertaining to passed address.
* <p>
* @return list of unspent outputs, or empty list if address unknown
* @throws ForeignBlockchainException if there was an error.
*/
@Override
public List<UnspentOutput> getUnspentOutputs(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
GetAddressUtxosArg getAddressUtxosArg = GetAddressUtxosArg.newBuilder().addAddresses(address).build();
GetAddressUtxosReplyList replyList = this.getCompactTxStreamerStub().getAddressUtxos(getAddressUtxosArg);
if (!(replyList instanceof GetAddressUtxosReplyList))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getUnspentOutputs gRPC");
List<GetAddressUtxosReply> unspentList = replyList.getAddressUtxosList();
if (unspentList == null)
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getUnspentOutputs gRPC");
List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (GetAddressUtxosReply unspent : unspentList) {
int height = (int)unspent.getHeight();
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
if (!includeUnconfirmed && height <= 0)
continue;
byte[] txHash = unspent.getTxid().toByteArray();
int outputIndex = unspent.getIndex();
long value = unspent.getValueZat();
byte[] script = unspent.getScript().toByteArray();
String addressRes = unspent.getAddress();
unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value, script, addressRes));
}
return unspentOutputs;
}
/**
* Returns list of unspent outputs pertaining to passed payment script.
* <p>
* @return list of unspent outputs, or empty list if script unknown
* @throws ForeignBlockchainException if there was an error.
*/
@Override
public List<UnspentOutput> getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
String address = this.blockchain.deriveP2shAddress(script);
return this.getUnspentOutputs(address, includeUnconfirmed);
}
/**
* Returns raw transaction for passed transaction hash.
* <p>
* NOTE: Do not mutate returned byte[]!
*
* @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws ForeignBlockchainException if error occurs
*/
@Override
public byte[] getRawTransaction(String txHash) throws ForeignBlockchainException {
return getRawTransaction(HashCode.fromString(txHash).asBytes());
}
/**
* Returns raw transaction for passed transaction hash.
* <p>
* NOTE: Do not mutate returned byte[]!
*
* @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws ForeignBlockchainException if error occurs
*/
@Override
public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException {
ByteString byteString = ByteString.copyFrom(txHash);
TxFilter txFilter = TxFilter.newBuilder().setHash(byteString).build();
RawTransaction rawTransaction = this.getCompactTxStreamerStub().getTransaction(txFilter);
if (!(rawTransaction instanceof RawTransaction))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getTransaction gRPC");
return rawTransaction.getData().toByteArray();
}
/**
* Returns transaction info for passed transaction hash.
* <p>
* @throws ForeignBlockchainException.NotFoundException if transaction not found
* @throws ForeignBlockchainException if error occurs
*/
@Override
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
// Check cache first
BitcoinyTransaction transaction = transactionCache.get(txHash);
if (transaction != null)
return transaction;
ByteString byteString = ByteString.copyFrom(HashCode.fromString(txHash).asBytes());
TxFilter txFilter = TxFilter.newBuilder().setHash(byteString).build();
RawTransaction rawTransaction = this.getCompactTxStreamerStub().getTransaction(txFilter);
if (!(rawTransaction instanceof RawTransaction))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain getTransaction gRPC");
byte[] transactionData = rawTransaction.getData().toByteArray();
String transactionDataString = HashCode.fromBytes(transactionData).toString();
JSONParser parser = new JSONParser();
JSONObject transactionJson;
try {
transactionJson = (JSONObject) parser.parse(transactionDataString);
} catch (ParseException e) {
throw new ForeignBlockchainException.NetworkException("Expected JSON string from Pirate Chain getTransaction gRPC");
}
Object inputsObj = transactionJson.get("vin");
if (!(inputsObj instanceof JSONArray))
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vin' from Pirate Chain getTransaction gRPC");
Object outputsObj = transactionJson.get("vout");
if (!(outputsObj instanceof JSONArray))
throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vout' from Pirate Chain getTransaction gRPC");
try {
int size = ((Long) transactionJson.get("size")).intValue();
int locktime = ((Long) transactionJson.get("locktime")).intValue();
// Timestamp might not be present, e.g. for unconfirmed transaction
Object timeObj = transactionJson.get("time");
Integer timestamp = timeObj != null
? ((Long) timeObj).intValue()
: null;
List<BitcoinyTransaction.Input> inputs = new ArrayList<>();
for (Object inputObj : (JSONArray) inputsObj) {
JSONObject inputJson = (JSONObject) inputObj;
String scriptSig = (String) ((JSONObject) inputJson.get("scriptSig")).get("hex");
int sequence = ((Long) inputJson.get("sequence")).intValue();
String outputTxHash = (String) inputJson.get("txid");
int outputVout = ((Long) inputJson.get("vout")).intValue();
inputs.add(new BitcoinyTransaction.Input(scriptSig, sequence, outputTxHash, outputVout));
}
List<BitcoinyTransaction.Output> outputs = new ArrayList<>();
for (Object outputObj : (JSONArray) outputsObj) {
JSONObject outputJson = (JSONObject) outputObj;
String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex");
long value = BigDecimal.valueOf((Double) outputJson.get("value")).setScale(8).unscaledValue().longValue();
// address too, if present in the "addresses" array
List<String> addresses = null;
Object addressesObj = ((JSONObject) outputJson.get("scriptPubKey")).get("addresses");
if (addressesObj instanceof JSONArray) {
addresses = new ArrayList<>();
for (Object addressObj : (JSONArray) addressesObj) {
addresses.add((String) addressObj);
}
}
// some peers return a single "address" string
Object addressObj = ((JSONObject) outputJson.get("scriptPubKey")).get("address");
if (addressObj instanceof String) {
if (addresses == null) {
addresses = new ArrayList<>();
}
addresses.add((String) addressObj);
}
// For the purposes of Qortal we require all outputs to contain addresses
// Some servers omit this info, causing problems down the line with balance calculations
// Update: it turns out that they were just using a different key - "address" instead of "addresses"
// The code below can remain in place, just in case a peer returns a missing address in the future
if (addresses == null || addresses.isEmpty()) {
if (this.currentServer != null) {
this.uselessServers.add(this.currentServer);
this.closeServer(this.currentServer);
}
LOGGER.info("No output addresses returned for transaction {}", txHash);
throw new ForeignBlockchainException(String.format("No output addresses returned for transaction %s", txHash));
}
outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses));
}
transaction = new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs);
// Save into cache
transactionCache.put(txHash, transaction);
return transaction;
} catch (NullPointerException | ClassCastException e) {
// Unexpected / invalid response from ElectrumX server
}
throw new ForeignBlockchainException.NetworkException("Unexpected JSON format from Pirate Chain getTransaction gRPC");
}
/**
* Returns list of transactions, relating to passed payment script.
* <p>
* @return list of related transactions, or empty list if script unknown
* @throws ForeignBlockchainException if error occurs
*/
@Override
public List<TransactionHash> getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException {
// FUTURE: implement this if needed. Probably not very useful for private blockchains.
throw new ForeignBlockchainException("getAddressTransactions not yet implemented for Pirate Chain");
}
@Override
public List<BitcoinyTransaction> getAddressBitcoinyTransactions(String address, boolean includeUnconfirmed) throws ForeignBlockchainException {
try {
// Firstly we need to get the latest block
int defaultBirthday = Settings.getInstance().getArrrDefaultBirthday();
BlockID endBlock = this.getCompactTxStreamerStub().getLatestBlock(null);
BlockID startBlock = BlockID.newBuilder().setHeight(defaultBirthday).build();
BlockRange blockRange = BlockRange.newBuilder().setStart(startBlock).setEnd(endBlock).build();
TransparentAddressBlockFilter blockFilter = TransparentAddressBlockFilter.newBuilder()
.setAddress(address)
.setRange(blockRange)
.build();
Iterator<Service.RawTransaction> transactionIterator = this.getCompactTxStreamerStub().getTaddressTxids(blockFilter);
// Map from Iterator to List
List<RawTransaction> rawTransactions = new ArrayList<>();
transactionIterator.forEachRemaining(rawTransactions::add);
List<BitcoinyTransaction> transactions = new ArrayList<>();
for (RawTransaction rawTransaction : rawTransactions) {
Long height = rawTransaction.getHeight();
if (!includeUnconfirmed && (height == null || height == 0))
// We only want confirmed transactions
continue;
byte[] transactionData = rawTransaction.getData().toByteArray();
String transactionDataHex = HashCode.fromBytes(transactionData).toString();
BitcoinyTransaction bitcoinyTransaction = PirateChain.deserializeRawTransaction(transactionDataHex);
bitcoinyTransaction.height = height.intValue();
transactions.add(bitcoinyTransaction);
}
return transactions;
}
catch (RuntimeException | TransformationException e) {
throw new ForeignBlockchainException(String.format("Unable to get transactions for address %s: %s", address, e.getMessage()));
}
}
/**
* Broadcasts raw transaction to network.
* <p>
* @throws ForeignBlockchainException if error occurs
*/
@Override
public void broadcastTransaction(byte[] transactionBytes) throws ForeignBlockchainException {
ByteString byteString = ByteString.copyFrom(transactionBytes);
RawTransaction rawTransaction = RawTransaction.newBuilder().setData(byteString).build();
SendResponse sendResponse = this.getCompactTxStreamerStub().sendTransaction(rawTransaction);
if (!(sendResponse instanceof SendResponse))
throw new ForeignBlockchainException.NetworkException("Unexpected output from Pirate Chain broadcastTransaction gRPC");
if (sendResponse.getErrorCode() != 0)
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error code from Pirate Chain broadcastTransaction gRPC: %d", sendResponse.getErrorCode()));
}
// Class-private utility methods
/**
* Performs RPC call, with automatic reconnection to different server if needed.
* <p>
* @return "result" object from within JSON output
* @throws ForeignBlockchainException if server returns error or something goes wrong
*/
private CompactTxStreamerGrpc.CompactTxStreamerBlockingStub getCompactTxStreamerStub() throws ForeignBlockchainException {
synchronized (this.serverLock) {
if (this.remainingServers.isEmpty())
this.remainingServers.addAll(this.servers);
while (haveConnection()) {
// If we have more servers and the last one replied slowly, try another
if (!this.remainingServers.isEmpty()) {
long averageResponseTime = this.currentServer.averageResponseTime();
if (averageResponseTime > MAX_AVG_RESPONSE_TIME) {
LOGGER.info("Slow average response time {}ms from {} - trying another server...", averageResponseTime, this.currentServer.hostname);
this.closeServer();
continue;
}
}
return CompactTxStreamerGrpc.newBlockingStub(this.channel);
// // Didn't work, try another server...
// this.closeServer();
}
// Failed to perform RPC - maybe lack of servers?
LOGGER.info("Error: No connected Pirate Light servers when trying to make RPC call");
throw new ForeignBlockchainException.NetworkException("No connected Pirate Light servers when trying to make RPC call");
}
}
/** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() throws ForeignBlockchainException {
if (this.currentServer != null && this.channel != null && !this.channel.isShutdown())
return true;
while (!this.remainingServers.isEmpty()) {
Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
this.channel = ManagedChannelBuilder.forAddress(server.hostname, server.port).build();
CompactTxStreamerGrpc.CompactTxStreamerBlockingStub stub = CompactTxStreamerGrpc.newBlockingStub(this.channel);
LightdInfo lightdInfo = stub.getLightdInfo(Empty.newBuilder().build());
if (lightdInfo == null || lightdInfo.getBlockHeight() <= 0)
continue;
// TODO: find a way to verify that the server is using the expected chain
// if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION)
// continue;
// if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash))
// continue;
LOGGER.debug(() -> String.format("Connected to %s", server));
this.currentServer = server;
return true;
} catch (Exception e) {
// Didn't work, try another server...
closeServer();
}
}
return false;
}
/**
* Closes connection to <tt>server</tt> if it is currently connected server.
* @param server
*/
private void closeServer(Server server) {
synchronized (this.serverLock) {
if (this.currentServer == null || !this.currentServer.equals(server) || this.channel == null) {
return;
}
// Close the gRPC managed-channel if not shut down already.
if (!this.channel.isShutdown()) {
try {
this.channel.shutdown();
if (!this.channel.awaitTermination(10, TimeUnit.SECONDS)) {
LOGGER.warn("Timed out gracefully shutting down connection: {}. ", this.channel);
}
} catch (Exception e) {
LOGGER.error("Unexpected exception while waiting for channel termination", e);
}
}
// Forceful shut down if still not terminated.
if (!this.channel.isTerminated()) {
try {
this.channel.shutdownNow();
if (!this.channel.awaitTermination(15, TimeUnit.SECONDS)) {
LOGGER.warn("Timed out forcefully shutting down connection: {}. ", this.channel);
}
} catch (Exception e) {
LOGGER.error("Unexpected exception while waiting for channel termination", e);
}
}
this.channel = null;
this.currentServer = null;
}
}
/** Closes connection to currently connected server (if any). */
private void closeServer() {
synchronized (this.serverLock) {
this.closeServer(this.currentServer);
}
}
}

View File

@@ -0,0 +1,409 @@
package org.qortal.crosschain;
import com.rust.litewalletjni.LiteWalletJni;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.DecoderException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.qortal.controller.PirateChainWalletController;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Random;
public class PirateWallet {
protected static final Logger LOGGER = LogManager.getLogger(PirateWallet.class);
private byte[] entropyBytes;
private final boolean isNullSeedWallet;
private String seedPhrase;
private boolean ready = false;
private String params;
private String saplingOutput64;
private String saplingSpend64;
private final static String COIN_PARAMS_FILENAME = "coinparams.json";
private final static String SAPLING_OUTPUT_FILENAME = "saplingoutput_base64";
private final static String SAPLING_SPEND_FILENAME = "saplingspend_base64";
public PirateWallet(byte[] entropyBytes, boolean isNullSeedWallet) throws IOException {
this.entropyBytes = entropyBytes;
this.isNullSeedWallet = isNullSeedWallet;
Path libDirectory = PirateChainWalletController.getRustLibOuterDirectory();
if (!Files.exists(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME))) {
return;
}
this.params = Files.readString(Paths.get(libDirectory.toString(), COIN_PARAMS_FILENAME));
this.saplingOutput64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_OUTPUT_FILENAME));
this.saplingSpend64 = Files.readString(Paths.get(libDirectory.toString(), SAPLING_SPEND_FILENAME));
this.ready = this.initialize();
}
private boolean initialize() {
try {
LiteWalletJni.initlogging();
if (this.entropyBytes == null) {
return false;
}
// Pick a random server
PirateLightClient.Server server = this.getRandomServer();
String serverUri = String.format("https://%s:%d/", server.hostname, server.port);
// Pirate library uses base64 encoding
String entropy64 = Base64.toBase64String(this.entropyBytes);
// Derive seed phrase from entropy bytes
String inputSeedResponse = LiteWalletJni.getseedphrasefromentropyb64(entropy64);
JSONObject inputSeedJson = new JSONObject(inputSeedResponse);
String inputSeedPhrase = null;
if (inputSeedJson.has("seedPhrase")) {
inputSeedPhrase = inputSeedJson.getString("seedPhrase");
}
String wallet = this.load();
if (wallet == null) {
// Wallet doesn't exist, so create a new one
int birthday = Settings.getInstance().getArrrDefaultBirthday();
if (this.isNullSeedWallet) {
try {
// Attempt to set birthday to the current block for null seed wallets
birthday = PirateChain.getInstance().blockchainProvider.getCurrentHeight();
}
catch (ForeignBlockchainException e) {
// Use the default height
}
}
// Initialize new wallet
String birthdayString = String.format("%d", birthday);
String outputSeedResponse = LiteWalletJni.initfromseed(serverUri, this.params, inputSeedPhrase, birthdayString, this.saplingOutput64, this.saplingSpend64); // Thread-safe.
JSONObject outputSeedJson = new JSONObject(outputSeedResponse);
String outputSeedPhrase = null;
if (outputSeedJson.has("seed")) {
outputSeedPhrase = outputSeedJson.getString("seed");
}
// Ensure seed phrase in response matches supplied seed phrase
if (inputSeedPhrase == null || !Objects.equals(inputSeedPhrase, outputSeedPhrase)) {
LOGGER.info("Unable to initialize Pirate Chain wallet: seed phrases do not match, or are null");
return false;
}
this.seedPhrase = outputSeedPhrase;
} else {
// Restore existing wallet
String response = LiteWalletJni.initfromb64(serverUri, params, wallet, saplingOutput64, saplingSpend64);
if (response != null && !response.contains("\"initalized\":true")) {
LOGGER.info("Unable to initialize Pirate Chain wallet: {}", response);
return false;
}
this.seedPhrase = inputSeedPhrase;
}
// Check that we're able to communicate with the library
Integer ourHeight = this.getHeight();
return (ourHeight != null && ourHeight > 0);
} catch (IOException | JSONException | UnsatisfiedLinkError e) {
LOGGER.info("Unable to initialize Pirate Chain wallet: {}", e.getMessage());
}
return false;
}
public boolean isReady() {
return this.ready;
}
public void setReady(boolean ready) {
this.ready = ready;
}
public boolean entropyBytesEqual(byte[] testEntropyBytes) {
return Arrays.equals(testEntropyBytes, this.entropyBytes);
}
private void encrypt() {
if (this.isEncrypted()) {
// Nothing to do
return;
}
String encryptionKey = this.getEncryptionKey();
if (encryptionKey == null) {
// Can't encrypt without a key
return;
}
this.doEncrypt(encryptionKey);
}
private void decrypt() {
if (!this.isEncrypted()) {
// Nothing to do
return;
}
String encryptionKey = this.getEncryptionKey();
if (encryptionKey == null) {
// Can't encrypt without a key
return;
}
this.doDecrypt(encryptionKey);
}
public void unlock() {
if (!this.isEncrypted()) {
// Nothing to do
return;
}
String encryptionKey = this.getEncryptionKey();
if (encryptionKey == null) {
// Can't encrypt without a key
return;
}
this.doUnlock(encryptionKey);
}
public boolean save() throws IOException {
if (!isInitialized()) {
LOGGER.info("Error: can't save wallet, because no wallet it initialized");
return false;
}
if (this.isNullSeedWallet()) {
// Don't save wallets that have a null seed
return false;
}
// Encrypt first (will do nothing if already encrypted)
this.encrypt();
String wallet64 = LiteWalletJni.save();
byte[] wallet;
try {
wallet = Base64.decode(wallet64);
}
catch (DecoderException e) {
LOGGER.info("Unable to decode wallet");
return false;
}
if (wallet == null) {
LOGGER.info("Unable to save wallet");
return false;
}
Path walletPath = this.getCurrentWalletPath();
Files.createDirectories(walletPath.getParent());
Files.write(walletPath, wallet, StandardOpenOption.CREATE);
LOGGER.debug("Saved Pirate Chain wallet");
return true;
}
public String load() throws IOException {
if (this.isNullSeedWallet()) {
// Don't load wallets that have a null seed
return null;
}
Path walletPath = this.getCurrentWalletPath();
if (!Files.exists(walletPath)) {
return null;
}
byte[] wallet = Files.readAllBytes(walletPath);
if (wallet == null) {
return null;
}
String wallet64 = Base64.toBase64String(wallet);
return wallet64;
}
private String getEntropyHash58() {
if (this.entropyBytes == null) {
return null;
}
byte[] entropyHash = Crypto.digest(this.entropyBytes);
return Base58.encode(entropyHash);
}
public String getSeedPhrase() {
return this.seedPhrase;
}
private String getEncryptionKey() {
if (this.entropyBytes == null) {
return null;
}
// Prefix the bytes with a (deterministic) string, to ensure that the resulting hash is different
String prefix = "ARRRWalletEncryption";
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
outputStream.write(prefix.getBytes(StandardCharsets.UTF_8));
outputStream.write(this.entropyBytes);
} catch (IOException e) {
return null;
}
byte[] encryptionKeyHash = Crypto.digest(outputStream.toByteArray());
return Base58.encode(encryptionKeyHash);
}
private Path getCurrentWalletPath() {
String entropyHash58 = this.getEntropyHash58();
String filename = String.format("wallet-%s.dat", entropyHash58);
return Paths.get(Settings.getInstance().getWalletsPath(), "PirateChain", filename);
}
public boolean isInitialized() {
return this.entropyBytes != null && this.ready;
}
public boolean isSynchronized() {
Integer height = this.getHeight();
Integer chainTip = this.getChainTip();
if (height == null || chainTip == null) {
return false;
}
// Assume synchronized if within 2 blocks of the chain tip
return height >= (chainTip - 2);
}
// APIs
public Integer getHeight() {
String response = LiteWalletJni.execute("height", "");
JSONObject json = new JSONObject(response);
if (json.has("height")) {
return json.getInt("height");
}
return null;
}
public Integer getChainTip() {
String response = LiteWalletJni.execute("info", "");
JSONObject json = new JSONObject(response);
if (json.has("latest_block_height")) {
return json.getInt("latest_block_height");
}
return null;
}
public boolean isNullSeedWallet() {
return this.isNullSeedWallet;
}
public Boolean isEncrypted() {
String response = LiteWalletJni.execute("encryptionstatus", "");
JSONObject json = new JSONObject(response);
if (json.has("encrypted")) {
return json.getBoolean("encrypted");
}
return null;
}
public boolean doEncrypt(String key) {
String response = LiteWalletJni.execute("encrypt", key);
JSONObject json = new JSONObject(response);
String result = json.getString("result");
if (json.has("result")) {
return (Objects.equals(result, "success"));
}
return false;
}
public boolean doDecrypt(String key) {
String response = LiteWalletJni.execute("decrypt", key);
JSONObject json = new JSONObject(response);
String result = json.getString("result");
if (json.has("result")) {
return (Objects.equals(result, "success"));
}
return false;
}
public boolean doUnlock(String key) {
String response = LiteWalletJni.execute("unlock", key);
JSONObject json = new JSONObject(response);
String result = json.getString("result");
if (json.has("result")) {
return (Objects.equals(result, "success"));
}
return false;
}
public String getWalletAddress() {
// Get balance, which also contains wallet addresses
String response = LiteWalletJni.execute("balance", "");
JSONObject json = new JSONObject(response);
String address = null;
if (json.has("z_addresses")) {
JSONArray z_addresses = json.getJSONArray("z_addresses");
if (z_addresses != null && !z_addresses.isEmpty()) {
JSONObject firstAddress = z_addresses.getJSONObject(0);
if (firstAddress.has("address")) {
address = firstAddress.getString("address");
}
}
}
return address;
}
public String getPrivateKey() {
String response = LiteWalletJni.execute("export", "");
JSONArray addressesJson = new JSONArray(response);
if (!addressesJson.isEmpty()) {
JSONObject addressJson = addressesJson.getJSONObject(0);
if (addressJson.has("private_key")) {
//String address = addressJson.getString("address");
String privateKey = addressJson.getString("private_key");
//String viewingKey = addressJson.getString("viewing_key");
return privateKey;
}
}
return null;
}
public PirateLightClient.Server getRandomServer() {
PirateChain.PirateChainNet pirateChainNet = Settings.getInstance().getPirateChainNet();
Collection<PirateLightClient.Server> servers = pirateChainNet.getServers();
Random random = new Random();
int index = random.nextInt(servers.size());
return (PirateLightClient.Server) servers.toArray()[index];
}
}

View File

@@ -7,11 +7,12 @@ import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
public class SimpleTransaction {
private String txHash;
private Integer timestamp;
private Long timestamp;
private long totalAmount;
private long feeAmount;
private List<Input> inputs;
private List<Output> outputs;
private String memo;
@XmlAccessorType(XmlAccessType.FIELD)
@@ -74,20 +75,21 @@ public class SimpleTransaction {
public SimpleTransaction() {
}
public SimpleTransaction(String txHash, Integer timestamp, long totalAmount, long feeAmount, List<Input> inputs, List<Output> outputs) {
public SimpleTransaction(String txHash, Long timestamp, long totalAmount, long feeAmount, List<Input> inputs, List<Output> outputs, String memo) {
this.txHash = txHash;
this.timestamp = timestamp;
this.totalAmount = totalAmount;
this.feeAmount = feeAmount;
this.inputs = inputs;
this.outputs = outputs;
this.memo = memo;
}
public String getTxHash() {
return txHash;
}
public Integer getTimestamp() {
public Long getTimestamp() {
return timestamp;
}

View File

@@ -13,8 +13,8 @@ import org.qortal.utils.Triple;
public enum SupportedBlockchain {
BITCOIN(Arrays.asList(
Triple.valueOf(BitcoinACCTv1.NAME, BitcoinACCTv1.CODE_BYTES_HASH, BitcoinACCTv1::getInstance)
// Could add improved BitcoinACCTv2 here in the future
Triple.valueOf(BitcoinACCTv1.NAME, BitcoinACCTv1.CODE_BYTES_HASH, BitcoinACCTv1::getInstance),
Triple.valueOf(BitcoinACCTv3.NAME, BitcoinACCTv3.CODE_BYTES_HASH, BitcoinACCTv3::getInstance)
)) {
@Override
public ForeignBlockchain getInstance() {
@@ -23,13 +23,12 @@ public enum SupportedBlockchain {
@Override
public ACCT getLatestAcct() {
return BitcoinACCTv1.getInstance();
return BitcoinACCTv3.getInstance();
}
},
LITECOIN(Arrays.asList(
Triple.valueOf(LitecoinACCTv1.NAME, LitecoinACCTv1.CODE_BYTES_HASH, LitecoinACCTv1::getInstance),
Triple.valueOf(LitecoinACCTv2.NAME, LitecoinACCTv2.CODE_BYTES_HASH, LitecoinACCTv2::getInstance),
Triple.valueOf(LitecoinACCTv3.NAME, LitecoinACCTv3.CODE_BYTES_HASH, LitecoinACCTv3::getInstance)
)) {
@Override
@@ -45,7 +44,6 @@ public enum SupportedBlockchain {
DOGECOIN(Arrays.asList(
Triple.valueOf(DogecoinACCTv1.NAME, DogecoinACCTv1.CODE_BYTES_HASH, DogecoinACCTv1::getInstance),
Triple.valueOf(DogecoinACCTv2.NAME, DogecoinACCTv2.CODE_BYTES_HASH, DogecoinACCTv2::getInstance),
Triple.valueOf(DogecoinACCTv3.NAME, DogecoinACCTv3.CODE_BYTES_HASH, DogecoinACCTv3::getInstance)
)) {
@Override
@@ -85,6 +83,20 @@ public enum SupportedBlockchain {
public ACCT getLatestAcct() {
return RavencoinACCTv3.getInstance();
}
},
PIRATECHAIN(Arrays.asList(
Triple.valueOf(PirateChainACCTv3.NAME, PirateChainACCTv3.CODE_BYTES_HASH, PirateChainACCTv3::getInstance)
)) {
@Override
public ForeignBlockchain getInstance() {
return PirateChain.getInstance();
}
@Override
public ACCT getLatestAcct() {
return PirateChainACCTv3.getInstance();
}
};
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())

View File

@@ -7,10 +7,20 @@ public class UnspentOutput {
public final int height;
public final long value;
public UnspentOutput(byte[] hash, int index, int height, long value) {
// Optional fields returned by Pirate Light Client server
public final byte[] script;
public final String address;
public UnspentOutput(byte[] hash, int index, int height, long value, byte[] script, String address) {
this.hash = hash;
this.index = index;
this.height = height;
this.value = value;
this.script = script;
this.address = address;
}
public UnspentOutput(byte[] hash, int index, int height, long value) {
this(hash, index, height, value, null, null);
}
}

View File

@@ -1,99 +0,0 @@
package org.qortal.crypto;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.math.ec.rfc7748.X25519;
import org.bouncycastle.math.ec.rfc7748.X25519Field;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
/** Additions to BouncyCastle providing Ed25519 to X25519 key conversion. */
public class BouncyCastle25519 {
private static final Class<?> pointAffineClass;
private static final Constructor<?> pointAffineCtor;
private static final Method decodePointVarMethod;
private static final Field yField;
static {
try {
Class<?> ed25519Class = Ed25519.class;
pointAffineClass = Arrays.stream(ed25519Class.getDeclaredClasses()).filter(clazz -> clazz.getSimpleName().equals("PointAffine")).findFirst().get();
if (pointAffineClass == null)
throw new ClassNotFoundException("Can't locate PointExt inner class inside Ed25519");
decodePointVarMethod = ed25519Class.getDeclaredMethod("decodePointVar", byte[].class, int.class, boolean.class, pointAffineClass);
decodePointVarMethod.setAccessible(true);
pointAffineCtor = pointAffineClass.getDeclaredConstructors()[0];
pointAffineCtor.setAccessible(true);
yField = pointAffineClass.getDeclaredField("y");
yField.setAccessible(true);
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException | NoSuchFieldException | ClassNotFoundException e) {
throw new RuntimeException("Can't initialize BouncyCastle25519 shim", e);
}
}
private static int[] obtainYFromPublicKey(byte[] ed25519PublicKey) {
try {
Object pA = pointAffineCtor.newInstance();
Boolean result = (Boolean) decodePointVarMethod.invoke(null, ed25519PublicKey, 0, true, pA);
if (result == null || !result)
return null;
return (int[]) yField.get(pA);
} catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException("Can't reflect into BouncyCastle", e);
}
}
public static byte[] toX25519PublicKey(byte[] ed25519PublicKey) {
int[] one = new int[X25519Field.SIZE];
X25519Field.one(one);
int[] y = obtainYFromPublicKey(ed25519PublicKey);
int[] oneMinusY = new int[X25519Field.SIZE];
X25519Field.sub(one, y, oneMinusY);
int[] onePlusY = new int[X25519Field.SIZE];
X25519Field.add(one, y, onePlusY);
int[] oneMinusYInverted = new int[X25519Field.SIZE];
X25519Field.inv(oneMinusY, oneMinusYInverted);
int[] u = new int[X25519Field.SIZE];
X25519Field.mul(onePlusY, oneMinusYInverted, u);
X25519Field.normalize(u);
byte[] x25519PublicKey = new byte[X25519.SCALAR_SIZE];
X25519Field.encode(u, x25519PublicKey, 0);
return x25519PublicKey;
}
public static byte[] toX25519PrivateKey(byte[] ed25519PrivateKey) {
Digest d = Ed25519.createPrehash();
byte[] h = new byte[d.getDigestSize()];
d.update(ed25519PrivateKey, 0, ed25519PrivateKey.length);
d.doFinal(h, 0);
byte[] s = new byte[X25519.SCALAR_SIZE];
System.arraycopy(h, 0, s, 0, X25519.SCALAR_SIZE);
s[0] &= 0xF8;
s[X25519.SCALAR_SIZE - 1] &= 0x7F;
s[X25519.SCALAR_SIZE - 1] |= 0x40;
return s;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -253,6 +253,10 @@ public abstract class Crypto {
return false;
}
public static byte[] toPublicKey(byte[] privateKey) {
return new Ed25519PrivateKeyParameters(privateKey, 0).generatePublicKey().getEncoded();
}
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
try {
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
@@ -264,16 +268,24 @@ public abstract class Crypto {
public static byte[] sign(Ed25519PrivateKeyParameters edPrivateKeyParams, byte[] message) {
byte[] signature = new byte[SIGNATURE_LENGTH];
edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519, edPrivateKeyParams.generatePublicKey(), null, message, 0, message.length, signature, 0);
edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519,null, message, 0, message.length, signature, 0);
return signature;
}
public static byte[] sign(byte[] privateKey, byte[] message) {
byte[] signature = new byte[SIGNATURE_LENGTH];
new Ed25519PrivateKeyParameters(privateKey, 0).sign(Ed25519.Algorithm.Ed25519,null, message, 0, message.length, signature, 0);
return signature;
}
public static byte[] getSharedSecret(byte[] privateKey, byte[] publicKey) {
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(privateKey);
byte[] x25519PrivateKey = Qortal25519Extras.toX25519PrivateKey(privateKey);
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
byte[] x25519PublicKey = Qortal25519Extras.toX25519PublicKey(publicKey);
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
@@ -281,5 +293,4 @@ public abstract class Crypto {
return sharedSecret;
}
}

View File

@@ -1,10 +1,44 @@
package org.qortal.crypto;
import org.qortal.utils.NTP;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException;
public class MemoryPoW {
/**
* Compute a MemoryPoW nonce
*
* @param data
* @param workBufferLength
* @param difficulty
* @return
* @throws TimeoutException
*/
public static Integer compute2(byte[] data, int workBufferLength, long difficulty) {
try {
return MemoryPoW.compute2(data, workBufferLength, difficulty, null);
} catch (TimeoutException e) {
// This won't happen, because above timeout is null
return null;
}
}
/**
* Compute a MemoryPoW nonce, with optional timeout
*
* @param data
* @param workBufferLength
* @param difficulty
* @param timeout maximum number of milliseconds to compute for before giving up,<br>or null if no timeout
* @return
* @throws TimeoutException
*/
public static Integer compute2(byte[] data, int workBufferLength, long difficulty, Long timeout) throws TimeoutException {
long startTime = NTP.getTime();
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
@@ -33,6 +67,13 @@ public class MemoryPoW {
if (Thread.currentThread().isInterrupted())
return -1;
if (timeout != null) {
long now = NTP.getTime();
if (now > startTime + timeout) {
throw new TimeoutException("Timeout reached");
}
}
seed *= seedMultiplier; // per nonce
state[0] = longHash[0] ^ seed;
@@ -58,6 +99,10 @@ public class MemoryPoW {
}
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
return verify2(data, null, workBufferLength, difficulty, nonce);
}
public static boolean verify2(byte[] data, long[] workBuffer, int workBufferLength, long difficulty, int nonce) {
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
@@ -70,7 +115,10 @@ public class MemoryPoW {
byteBuffer = null;
int longBufferLength = workBufferLength / 8;
long[] workBuffer = new long[longBufferLength];
if (workBuffer == null)
workBuffer = new long[longBufferLength];
long[] state = new long[4];
long seed = 8682522807148012L;

View File

@@ -0,0 +1,234 @@
package org.qortal.crypto;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.math.ec.rfc7748.X25519;
import org.bouncycastle.math.ec.rfc7748.X25519Field;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
import org.bouncycastle.math.raw.Nat256;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
/**
* Additions to BouncyCastle providing:
* <p></p>
* <ul>
* <li>Ed25519 to X25519 key conversion</li>
* <li>Aggregate public keys</li>
* <li>Aggregate signatures</li>
* </ul>
*/
public abstract class Qortal25519Extras extends BouncyCastleEd25519 {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public static byte[] toX25519PublicKey(byte[] ed25519PublicKey) {
int[] one = new int[X25519Field.SIZE];
X25519Field.one(one);
PointAffine pA = new PointAffine();
if (!decodePointVar(ed25519PublicKey, 0, true, pA))
return null;
int[] y = pA.y;
int[] oneMinusY = new int[X25519Field.SIZE];
X25519Field.sub(one, y, oneMinusY);
int[] onePlusY = new int[X25519Field.SIZE];
X25519Field.add(one, y, onePlusY);
int[] oneMinusYInverted = new int[X25519Field.SIZE];
X25519Field.inv(oneMinusY, oneMinusYInverted);
int[] u = new int[X25519Field.SIZE];
X25519Field.mul(onePlusY, oneMinusYInverted, u);
X25519Field.normalize(u);
byte[] x25519PublicKey = new byte[X25519.SCALAR_SIZE];
X25519Field.encode(u, x25519PublicKey, 0);
return x25519PublicKey;
}
public static byte[] toX25519PrivateKey(byte[] ed25519PrivateKey) {
Digest d = Ed25519.createPrehash();
byte[] h = new byte[d.getDigestSize()];
d.update(ed25519PrivateKey, 0, ed25519PrivateKey.length);
d.doFinal(h, 0);
byte[] s = new byte[X25519.SCALAR_SIZE];
System.arraycopy(h, 0, s, 0, X25519.SCALAR_SIZE);
s[0] &= 0xF8;
s[X25519.SCALAR_SIZE - 1] &= 0x7F;
s[X25519.SCALAR_SIZE - 1] |= 0x40;
return s;
}
// Mostly for test support
public static PointAccum newPointAccum() {
return new PointAccum();
}
public static byte[] aggregatePublicKeys(Collection<byte[]> publicKeys) {
PointAccum rAccum = null;
for (byte[] publicKey : publicKeys) {
PointAffine pA = new PointAffine();
if (!decodePointVar(publicKey, 0, false, pA))
// Failed to decode
return null;
if (rAccum == null) {
rAccum = new PointAccum();
pointCopy(pA, rAccum);
} else {
pointAdd(pointCopy(pA), rAccum);
}
}
byte[] publicKey = new byte[SCALAR_BYTES];
if (0 == encodePoint(rAccum, publicKey, 0))
// Failed to encode
return null;
return publicKey;
}
public static byte[] aggregateSignatures(Collection<byte[]> signatures) {
// Signatures are (R, s)
// R is a point
// s is a scalar
PointAccum rAccum = null;
int[] sAccum = new int[SCALAR_INTS];
byte[] rEncoded = new byte[POINT_BYTES];
int[] sPart = new int[SCALAR_INTS];
for (byte[] signature : signatures) {
System.arraycopy(signature,0, rEncoded, 0, rEncoded.length);
PointAffine pA = new PointAffine();
if (!decodePointVar(rEncoded, 0, false, pA))
// Failed to decode
return null;
if (rAccum == null) {
rAccum = new PointAccum();
pointCopy(pA, rAccum);
decode32(signature, rEncoded.length, sAccum, 0, SCALAR_INTS);
} else {
pointAdd(pointCopy(pA), rAccum);
decode32(signature, rEncoded.length, sPart, 0, SCALAR_INTS);
Nat256.addTo(sPart, sAccum);
// "mod L" on sAccum
if (Nat256.gte(sAccum, L))
Nat256.subFrom(L, sAccum);
}
}
byte[] signature = new byte[SIGNATURE_SIZE];
if (0 == encodePoint(rAccum, signature, 0))
// Failed to encode
return null;
for (int i = 0; i < sAccum.length; ++i) {
encode32(sAccum[i], signature, POINT_BYTES + i * 4);
}
return signature;
}
public static byte[] signForAggregation(byte[] privateKey, byte[] message) {
// Very similar to BouncyCastle's implementation except we use secure random nonce and different hash
Digest d = new SHA512Digest();
byte[] h = new byte[d.getDigestSize()];
d.reset();
d.update(privateKey, 0, privateKey.length);
d.doFinal(h, 0);
byte[] sH = new byte[SCALAR_BYTES];
pruneScalar(h, 0, sH);
byte[] publicKey = new byte[SCALAR_BYTES];
scalarMultBaseEncoded(sH, publicKey, 0);
byte[] rSeed = new byte[d.getDigestSize()];
SECURE_RANDOM.nextBytes(rSeed);
byte[] r = new byte[SCALAR_BYTES];
pruneScalar(rSeed, 0, r);
byte[] R = new byte[POINT_BYTES];
scalarMultBaseEncoded(r, R, 0);
d.reset();
d.update(message, 0, message.length);
d.doFinal(h, 0);
byte[] k = reduceScalar(h);
byte[] s = calculateS(r, k, sH);
byte[] signature = new byte[SIGNATURE_SIZE];
System.arraycopy(R, 0, signature, 0, POINT_BYTES);
System.arraycopy(s, 0, signature, POINT_BYTES, SCALAR_BYTES);
return signature;
}
public static boolean verifyAggregated(byte[] publicKey, byte[] signature, byte[] message) {
byte[] R = Arrays.copyOfRange(signature, 0, POINT_BYTES);
byte[] s = Arrays.copyOfRange(signature, POINT_BYTES, POINT_BYTES + SCALAR_BYTES);
if (!checkPointVar(R))
// R out of bounds
return false;
if (!checkScalarVar(s))
// s out of bounds
return false;
byte[] S = new byte[POINT_BYTES];
scalarMultBaseEncoded(s, S, 0);
PointAffine pA = new PointAffine();
if (!decodePointVar(publicKey, 0, true, pA))
// Failed to decode
return false;
Digest d = new SHA512Digest();
byte[] h = new byte[d.getDigestSize()];
d.update(message, 0, message.length);
d.doFinal(h, 0);
byte[] k = reduceScalar(h);
int[] nS = new int[SCALAR_INTS];
decodeScalar(s, 0, nS);
int[] nA = new int[SCALAR_INTS];
decodeScalar(k, 0, nA);
/*PointAccum*/
PointAccum pR = new PointAccum();
scalarMultStrausVar(nS, nA, pA, pR);
byte[] check = new byte[POINT_BYTES];
if (0 == encodePoint(pR, check, 0))
// Failed to encode
return false;
return Arrays.equals(check, R);
}
}

View File

@@ -24,7 +24,10 @@ public class ArbitraryResourceMetadata {
this.description = description;
this.tags = tags;
this.category = category;
this.categoryName = category.getName();
if (category != null) {
this.categoryName = category.getName();
}
}
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata) {

View File

@@ -26,6 +26,7 @@ public class ArbitraryResourceStatus {
}
}
private Status status;
private String id;
private String title;
private String description;
@@ -37,6 +38,7 @@ public class ArbitraryResourceStatus {
}
public ArbitraryResourceStatus(Status status, Integer localChunkCount, Integer totalChunkCount) {
this.status = status;
this.id = status.toString();
this.title = status.title;
this.description = status.description;
@@ -47,4 +49,20 @@ public class ArbitraryResourceStatus {
public ArbitraryResourceStatus(Status status) {
this(status, null, null);
}
public Status getStatus() {
return this.status;
}
public String getTitle() {
return this.title;
}
public Integer getLocalChunkCount() {
return this.localChunkCount;
}
public Integer getTotalChunkCount() {
return this.totalChunkCount;
}
}

View File

@@ -211,6 +211,14 @@ public class BlockData implements Serializable {
this.onlineAccountsSignatures = onlineAccountsSignatures;
}
public int getOnlineAccountsSignaturesCount() {
if (this.onlineAccountsSignatures != null && this.onlineAccountsSignatures.length > 0) {
// Blocks use a single online accounts signature, so there is no need for this to be dynamic
return 1;
}
return 0;
}
public boolean isTrimmed() {
long onlineAccountSignaturesTrimmedTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();

View File

@@ -11,11 +11,12 @@ public class BlockSummaryData {
private int height;
private byte[] signature;
private byte[] minterPublicKey;
private int onlineAccountsCount;
// Optional, set during construction
private Integer onlineAccountsCount;
private Long timestamp;
private Integer transactionCount;
private byte[] reference;
// Optional, set after construction
private Integer minterLevel;
@@ -25,6 +26,15 @@ public class BlockSummaryData {
protected BlockSummaryData() {
}
/** Constructor typically populated with fields from HeightV2Message */
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, long timestamp) {
this.height = height;
this.signature = signature;
this.minterPublicKey = minterPublicKey;
this.timestamp = timestamp;
}
/** Constructor typically populated with fields from BlockSummariesMessage */
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount) {
this.height = height;
this.signature = signature;
@@ -32,13 +42,16 @@ public class BlockSummaryData {
this.onlineAccountsCount = onlineAccountsCount;
}
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount, long timestamp, int transactionCount) {
/** Constructor typically populated with fields from BlockSummariesV2Message */
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, Integer onlineAccountsCount,
Long timestamp, Integer transactionCount, byte[] reference) {
this.height = height;
this.signature = signature;
this.minterPublicKey = minterPublicKey;
this.onlineAccountsCount = onlineAccountsCount;
this.timestamp = timestamp;
this.transactionCount = transactionCount;
this.reference = reference;
}
public BlockSummaryData(BlockData blockData) {
@@ -49,6 +62,7 @@ public class BlockSummaryData {
this.timestamp = blockData.getTimestamp();
this.transactionCount = blockData.getTransactionCount();
this.reference = blockData.getReference();
}
// Getters / setters
@@ -65,7 +79,7 @@ public class BlockSummaryData {
return this.minterPublicKey;
}
public int getOnlineAccountsCount() {
public Integer getOnlineAccountsCount() {
return this.onlineAccountsCount;
}
@@ -77,6 +91,10 @@ public class BlockSummaryData {
return this.transactionCount;
}
public byte[] getReference() {
return this.reference;
}
public Integer getMinterLevel() {
return this.minterLevel;
}

View File

@@ -1,7 +1,5 @@
package org.qortal.data.block;
import org.qortal.data.network.PeerChainTipData;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.math.BigInteger;
@@ -14,14 +12,14 @@ public class CommonBlockData {
private BlockSummaryData commonBlockSummary = null;
private List<BlockSummaryData> blockSummariesAfterCommonBlock = null;
private BigInteger chainWeight = null;
private PeerChainTipData chainTipData = null;
private BlockSummaryData chainTipData = null;
// Constructors
protected CommonBlockData() {
}
public CommonBlockData(BlockSummaryData commonBlockSummary, PeerChainTipData chainTipData) {
public CommonBlockData(BlockSummaryData commonBlockSummary, BlockSummaryData chainTipData) {
this.commonBlockSummary = commonBlockSummary;
this.chainTipData = chainTipData;
}
@@ -49,7 +47,7 @@ public class CommonBlockData {
this.chainWeight = chainWeight;
}
public PeerChainTipData getChainTipData() {
public BlockSummaryData getChainTipData() {
return this.chainTipData;
}

View File

@@ -27,6 +27,8 @@ public class ChatMessage {
private String recipientName;
private byte[] chatReference;
private byte[] data;
private boolean isText;
@@ -42,8 +44,8 @@ public class ChatMessage {
// For repository use
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
String senderName, String recipient, String recipientName, byte[] data, boolean isText,
boolean isEncrypted, byte[] signature) {
String senderName, String recipient, String recipientName, byte[] chatReference, byte[] data,
boolean isText, boolean isEncrypted, byte[] signature) {
this.timestamp = timestamp;
this.txGroupId = txGroupId;
this.reference = reference;
@@ -52,6 +54,7 @@ public class ChatMessage {
this.senderName = senderName;
this.recipient = recipient;
this.recipientName = recipientName;
this.chatReference = chatReference;
this.data = data;
this.isText = isText;
this.isEncrypted = isEncrypted;
@@ -90,6 +93,10 @@ public class ChatMessage {
return this.recipientName;
}
public byte[] getChatReference() {
return this.chatReference;
}
public byte[] getData() {
return this.data;
}

View File

@@ -5,6 +5,7 @@ import java.util.Arrays;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import org.qortal.account.PublicKeyAccount;
@@ -15,6 +16,10 @@ public class OnlineAccountData {
protected long timestamp;
protected byte[] signature;
protected byte[] publicKey;
protected Integer nonce;
@XmlTransient
private int hash;
// Constructors
@@ -22,10 +27,15 @@ public class OnlineAccountData {
protected OnlineAccountData() {
}
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey, Integer nonce) {
this.timestamp = timestamp;
this.signature = signature;
this.publicKey = publicKey;
this.nonce = nonce;
}
public OnlineAccountData(long timestamp, byte[] signature, byte[] publicKey) {
this(timestamp, signature, publicKey, null);
}
public long getTimestamp() {
@@ -40,6 +50,10 @@ public class OnlineAccountData {
return this.publicKey;
}
public Integer getNonce() {
return this.nonce;
}
// For JAXB
@XmlElement(name = "address")
protected String getAddress() {
@@ -62,20 +76,23 @@ public class OnlineAccountData {
if (otherOnlineAccountData.timestamp != this.timestamp)
return false;
// Signature more likely to be unique than public key
if (!Arrays.equals(otherOnlineAccountData.signature, this.signature))
return false;
if (!Arrays.equals(otherOnlineAccountData.publicKey, this.publicKey))
return false;
// We don't compare signature because it's not our remit to verify and newer aggregate signatures use random nonces
return true;
}
@Override
public int hashCode() {
// Pretty lazy implementation
return (int) this.timestamp;
int h = this.hash;
if (h == 0) {
this.hash = h = Long.hashCode(this.timestamp)
^ Arrays.hashCode(this.publicKey);
// We don't use signature because newer aggregate signatures use random nonces
}
return h;
}
}

View File

@@ -1,37 +0,0 @@
package org.qortal.data.network;
public class PeerChainTipData {
/** Latest block height as reported by peer. */
private Integer lastHeight;
/** Latest block signature as reported by peer. */
private byte[] lastBlockSignature;
/** Latest block timestamp as reported by peer. */
private Long lastBlockTimestamp;
/** Latest block minter public key as reported by peer. */
private byte[] lastBlockMinter;
public PeerChainTipData(Integer lastHeight, byte[] lastBlockSignature, Long lastBlockTimestamp, byte[] lastBlockMinter) {
this.lastHeight = lastHeight;
this.lastBlockSignature = lastBlockSignature;
this.lastBlockTimestamp = lastBlockTimestamp;
this.lastBlockMinter = lastBlockMinter;
}
public Integer getLastHeight() {
return this.lastHeight;
}
public byte[] getLastBlockSignature() {
return this.lastBlockSignature;
}
public Long getLastBlockTimestamp() {
return this.lastBlockTimestamp;
}
public byte[] getLastBlockMinter() {
return this.lastBlockMinter;
}
}

View File

@@ -28,6 +28,9 @@ public class PeerData {
private Long addedWhen;
private String addedBy;
/** The number of consecutive times we failed to sync with this peer */
private int failedSyncCount = 0;
// Constructors
// necessary for JAXB serialization
@@ -92,6 +95,18 @@ public class PeerData {
return this.addedBy;
}
public int getFailedSyncCount() {
return this.failedSyncCount;
}
public void setFailedSyncCount(int failedSyncCount) {
this.failedSyncCount = failedSyncCount;
}
public void incrementFailedSyncCount() {
this.failedSyncCount++;
}
// Pretty peerAddress getter for JAXB
@XmlElement(name = "address")
protected String getPrettyAddress() {

View File

@@ -26,6 +26,8 @@ public class ChatTransactionData extends TransactionData {
private String recipient; // can be null
private byte[] chatReference; // can be null
@Schema(description = "raw message data, possibly UTF8 text", example = "2yGEbwRFyhPZZckKA")
private byte[] data;
@@ -44,13 +46,14 @@ public class ChatTransactionData extends TransactionData {
}
public ChatTransactionData(BaseTransactionData baseTransactionData,
String sender, int nonce, String recipient, byte[] data, boolean isText, boolean isEncrypted) {
String sender, int nonce, String recipient, byte[] chatReference, byte[] data, boolean isText, boolean isEncrypted) {
super(TransactionType.CHAT, baseTransactionData);
this.senderPublicKey = baseTransactionData.creatorPublicKey;
this.sender = sender;
this.nonce = nonce;
this.recipient = recipient;
this.chatReference = chatReference;
this.data = data;
this.isText = isText;
this.isEncrypted = isEncrypted;
@@ -78,6 +81,10 @@ public class ChatTransactionData extends TransactionData {
return this.recipient;
}
public byte[] getChatReference() {
return this.chatReference;
}
public byte[] getData() {
return this.data;
}

View File

@@ -128,6 +128,10 @@ public abstract class TransactionData {
return this.txGroupId;
}
public void setTxGroupId(int txGroupId) {
this.txGroupId = txGroupId;
}
public byte[] getReference() {
return this.reference;
}

View File

@@ -80,6 +80,9 @@ public class Group {
// Useful constants
public static final int NO_GROUP = 0;
// Null owner address corresponds with public key "11111111111111111111111111111111"
public static String NULL_OWNER_ADDRESS = "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG";
public static final int MIN_NAME_SIZE = 3;
public static final int MAX_NAME_SIZE = 32;
public static final int MAX_DESCRIPTION_SIZE = 128;

View File

@@ -23,6 +23,7 @@ import java.util.List;
import javax.swing.JDialog;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.SwingWorker;
import javax.swing.event.PopupMenuEvent;
@@ -178,6 +179,14 @@ public class SysTray {
menu.add(syncTime);
}
JMenuItem about = new JMenuItem(Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"));
about.addActionListener(actionEvent -> {
destroyHiddenDialog();
JOptionPane.showMessageDialog(null,"Qortal Core\n" + Translator.INSTANCE.translate("SysTray", "BUILD_VERSION") + ":\n" + Controller.getInstance().getVersionStringWithoutPrefix(),"Qortal Core",1);
});
menu.add(about);
JMenuItem exit = new JMenuItem(Translator.INSTANCE.translate("SysTray", "EXIT"));
exit.addActionListener(actionEvent -> {
destroyHiddenDialog();

View File

@@ -195,7 +195,7 @@ public class Name {
this.repository.getNameRepository().save(this.nameData);
}
public void buy(BuyNameTransactionData buyNameTransactionData) throws DataException {
public void buy(BuyNameTransactionData buyNameTransactionData, boolean modifyBalances) throws DataException {
// Save previous name-changing reference in this transaction's data
// Caller is expected to save
buyNameTransactionData.setNameReference(this.nameData.getReference());
@@ -203,15 +203,20 @@ public class Name {
// Mark not for-sale but leave price in case we want to orphan
this.nameData.setIsForSale(false);
// Update seller's balance
Account seller = new Account(this.repository, this.nameData.getOwner());
seller.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount());
if (modifyBalances) {
// Update seller's balance
Account seller = new Account(this.repository, this.nameData.getOwner());
seller.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount());
}
// Set new owner
Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey());
this.nameData.setOwner(buyer.getAddress());
// Update buyer's balance
buyer.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount());
if (modifyBalances) {
// Update buyer's balance
buyer.modifyAssetBalance(Asset.QORT, -buyNameTransactionData.getAmount());
}
// Set name-changing reference to this transaction
this.nameData.setReference(buyNameTransactionData.getSignature());

View File

@@ -8,8 +8,10 @@ import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.qortal.block.BlockChain;
import org.qortal.controller.Controller;
import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.message.*;
@@ -89,6 +91,8 @@ public class Network {
private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds
private static final int BROADCAST_CHAIN_TIP_DEPTH = 7; // Just enough to fill a SINGLE TCP packet (~1440 bytes)
// Generate our node keys / ID
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
@@ -259,6 +263,18 @@ public class Network {
return this.immutableConnectedPeers;
}
public List<Peer> getImmutableConnectedDataPeers() {
return this.getImmutableConnectedPeers().stream()
.filter(p -> p.isDataPeer())
.collect(Collectors.toList());
}
public List<Peer> getImmutableConnectedNonDataPeers() {
return this.getImmutableConnectedPeers().stream()
.filter(p -> !p.isDataPeer())
.collect(Collectors.toList());
}
public void addConnectedPeer(Peer peer) {
this.connectedPeers.add(peer); // thread safe thanks to synchronized list
this.immutableConnectedPeers = List.copyOf(this.connectedPeers); // also thread safe thanks to synchronized collection's toArray() being fed to List.of(array)
@@ -325,6 +341,7 @@ public class Network {
// Add this signature to the list of pending requests for this peer
LOGGER.info("Making connection to peer {} to request files for signature {}...", peerAddressString, Base58.encode(signature));
Peer peer = new Peer(peerData);
peer.setIsDataPeer(true);
peer.addPendingSignatureRequest(signature);
return this.connectPeer(peer);
// If connection (and handshake) is successful, data will automatically be requested
@@ -455,6 +472,8 @@ public class Network {
class NetworkProcessor extends ExecuteProduceConsume {
private final Logger LOGGER = LogManager.getLogger(NetworkProcessor.class);
private final AtomicLong nextConnectTaskTimestamp = new AtomicLong(0L); // ms - try first connect once NTP syncs
private final AtomicLong nextBroadcastTimestamp = new AtomicLong(0L); // ms - try first broadcast once NTP syncs
@@ -685,6 +704,7 @@ public class Network {
// Pick candidate
PeerData peerData = peers.get(peerIndex);
Peer newPeer = new Peer(peerData);
newPeer.setIsDataPeer(false);
// Update connection attempt info
peerData.setLastAttempted(now);
@@ -1069,11 +1089,19 @@ public class Network {
// (If inbound sent anything here, it's possible it could be processed out-of-order with handshake message).
if (peer.isOutbound()) {
// Send our height
Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
if (!peer.sendMessage(heightMessage)) {
peer.disconnect("failed to send height/info");
return;
if (!Settings.getInstance().isLite()) {
// Send our height / chain tip info
Message message = this.buildHeightOrChainTipInfo(peer);
if (message == null) {
peer.disconnect("Couldn't build our chain tip info");
return;
}
if (!peer.sendMessage(message)) {
peer.disconnect("failed to send height / chain tip info");
return;
}
}
// Send our peers list
@@ -1145,10 +1173,47 @@ public class Network {
return new PeersV2Message(peerAddresses);
}
public Message buildHeightMessage(Peer peer, BlockData blockData) {
// HEIGHT_V2 contains way more useful info
return new HeightV2Message(blockData.getHeight(), blockData.getSignature(),
blockData.getTimestamp(), blockData.getMinterPublicKey());
/** Builds either (legacy) HeightV2Message or (newer) BlockSummariesV2Message, depending on peer version.
*
* @return Message, or null if DataException was thrown.
*/
public Message buildHeightOrChainTipInfo(Peer peer) {
if (peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION) {
int latestHeight = Controller.getInstance().getChainHeight();
try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockSummaryData> latestBlockSummaries = repository.getBlockRepository().getBlockSummaries(latestHeight - BROADCAST_CHAIN_TIP_DEPTH, latestHeight);
return new BlockSummariesV2Message(latestBlockSummaries);
} catch (DataException e) {
return null;
}
} else {
// For older peers
BlockData latestBlockData = Controller.getInstance().getChainTip();
return new HeightV2Message(latestBlockData.getHeight(), latestBlockData.getSignature(),
latestBlockData.getTimestamp(), latestBlockData.getMinterPublicKey());
}
}
public void broadcastOurChain() {
BlockData latestBlockData = Controller.getInstance().getChainTip();
int latestHeight = latestBlockData.getHeight();
try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockSummaryData> latestBlockSummaries = repository.getBlockRepository().getBlockSummaries(latestHeight - BROADCAST_CHAIN_TIP_DEPTH, latestHeight);
Message latestBlockSummariesMessage = new BlockSummariesV2Message(latestBlockSummaries);
// For older peers
Message heightMessage = new HeightV2Message(latestBlockData.getHeight(), latestBlockData.getSignature(),
latestBlockData.getTimestamp(), latestBlockData.getMinterPublicKey());
Network.getInstance().broadcast(broadcastPeer -> broadcastPeer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
? latestBlockSummariesMessage
: heightMessage
);
} catch (DataException e) {
LOGGER.warn("Couldn't broadcast our chain tip info", e);
}
}
public Message buildNewTransactionMessage(Peer peer, TransactionData transactionData) {

View File

@@ -6,12 +6,13 @@ import com.google.common.net.InetAddresses;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.block.CommonBlockData;
import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.network.PeerData;
import org.qortal.network.message.ChallengeMessage;
import org.qortal.network.message.Message;
import org.qortal.network.message.MessageException;
import org.qortal.network.message.MessageType;
import org.qortal.network.task.MessageTask;
import org.qortal.network.task.PingTask;
import org.qortal.settings.Settings;
@@ -26,6 +27,8 @@ import java.nio.channels.SocketChannel;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -64,6 +67,11 @@ public class Peer {
*/
private boolean isLocal;
/**
* True if connected for the purposes of transfering specific QDN data
*/
private boolean isDataPeer;
private final UUID peerConnectionId = UUID.randomUUID();
private final Object byteBufferLock = new Object();
private ByteBuffer byteBuffer;
@@ -140,13 +148,23 @@ public class Peer {
/**
* Latest block info as reported by peer.
*/
private PeerChainTipData peersChainTipData;
private List<BlockSummaryData> peersChainTipData = Collections.emptyList();
/**
* Our common block with this peer
*/
private CommonBlockData commonBlockData;
// Message stats
private static class MessageStats {
public final LongAdder count = new LongAdder();
public final LongAdder totalBytes = new LongAdder();
}
private final Map<MessageType, MessageStats> receivedMessageStats = new ConcurrentHashMap<>();
private final Map<MessageType, MessageStats> sentMessageStats = new ConcurrentHashMap<>();
// Constructors
/**
@@ -194,6 +212,14 @@ public class Peer {
return this.isOutbound;
}
public boolean isDataPeer() {
return isDataPeer;
}
public void setIsDataPeer(boolean isDataPeer) {
this.isDataPeer = isDataPeer;
}
public Handshake getHandshakeStatus() {
synchronized (this.handshakingLock) {
return this.handshakeStatus;
@@ -211,6 +237,11 @@ public class Peer {
}
private void generateRandomMaxConnectionAge() {
if (this.maxConnectionAge > 0L) {
// Already generated, so we don't want to overwrite the existing value
return;
}
// Retrieve the min and max connection time from the settings, and calculate the range
final int minPeerConnectionTime = Settings.getInstance().getMinPeerConnectionTime();
final int maxPeerConnectionTime = Settings.getInstance().getMaxPeerConnectionTime();
@@ -322,28 +353,34 @@ public class Peer {
}
}
public PeerChainTipData getChainTipData() {
synchronized (this.peerInfoLock) {
return this.peersChainTipData;
}
public BlockSummaryData getChainTipData() {
List<BlockSummaryData> chainTipSummaries = this.peersChainTipData;
if (chainTipSummaries.isEmpty())
return null;
// Return last entry, which should have greatest height
return chainTipSummaries.get(chainTipSummaries.size() - 1);
}
public void setChainTipData(PeerChainTipData chainTipData) {
synchronized (this.peerInfoLock) {
this.peersChainTipData = chainTipData;
}
public void setChainTipData(BlockSummaryData chainTipData) {
this.peersChainTipData = Collections.singletonList(chainTipData);
}
public List<BlockSummaryData> getChainTipSummaries() {
return this.peersChainTipData;
}
public void setChainTipSummaries(List<BlockSummaryData> chainTipSummaries) {
this.peersChainTipData = List.copyOf(chainTipSummaries);
}
public CommonBlockData getCommonBlockData() {
synchronized (this.peerInfoLock) {
return this.commonBlockData;
}
return this.commonBlockData;
}
public void setCommonBlockData(CommonBlockData commonBlockData) {
synchronized (this.peerInfoLock) {
this.commonBlockData = commonBlockData;
}
this.commonBlockData = commonBlockData;
}
public boolean isSyncInProgress() {
@@ -523,11 +560,22 @@ public class Peer {
// Tidy up buffers:
this.byteBuffer.flip();
// Read-only, flipped buffer's position will be after end of message, so copy that
long messageByteSize = readOnlyBuffer.position();
this.byteBuffer.position(readOnlyBuffer.position());
// Copy bytes after read message to front of buffer,
// adjusting position accordingly, reset limit to capacity
this.byteBuffer.compact();
// Record message stats
MessageStats messageStats = this.receivedMessageStats.computeIfAbsent(message.getType(), k -> new MessageStats());
// Ideally these two operations would be atomic, we could pack 'count' in top X bits of the 64-bit long, but meh
messageStats.count.increment();
messageStats.totalBytes.add(messageByteSize);
// Unsupported message type? Discard with no further processing
if (message.getType() == MessageType.UNSUPPORTED)
continue;
BlockingQueue<Message> queue = this.replyQueues.get(message.getId());
if (queue != null) {
// Adding message to queue will unblock thread waiting for response
@@ -586,6 +634,12 @@ public class Peer {
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}",
this.peerConnectionId, this.outputMessageType, this.outputMessageId, this);
// Record message stats
MessageStats messageStats = this.sentMessageStats.computeIfAbsent(message.getType(), k -> new MessageStats());
// Ideally these two operations would be atomic, we could pack 'count' in top X bits of the 64-bit long, but meh
messageStats.count.increment();
messageStats.totalBytes.add(this.outputBuffer.limit());
} catch (MessageException e) {
// Something went wrong converting message to bytes, so discard but allow another round
LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
@@ -776,8 +830,11 @@ public class Peer {
}
public void shutdown() {
boolean logStats = false;
if (!isStopping) {
LOGGER.debug("[{}] Shutting down peer {}", this.peerConnectionId, this);
logStats = true;
}
isStopping = true;
@@ -789,8 +846,34 @@ public class Peer {
LOGGER.debug("[{}] IOException while trying to close peer {}", this.peerConnectionId, this);
}
}
if (logStats && this.receivedMessageStats.size() > 0) {
StringBuilder statsBuilder = new StringBuilder(1024);
statsBuilder.append("peer ").append(this).append(" message stats:\n=received=");
appendMessageStats(statsBuilder, this.receivedMessageStats);
statsBuilder.append("\n=sent=");
appendMessageStats(statsBuilder, this.sentMessageStats);
LOGGER.debug(statsBuilder.toString());
}
}
private static void appendMessageStats(StringBuilder statsBuilder, Map<MessageType, MessageStats> messageStats) {
if (messageStats.isEmpty()) {
statsBuilder.append("\n none");
return;
}
messageStats.keySet().stream()
.sorted(Comparator.comparing(MessageType::name))
.forEach(messageType -> {
MessageStats stats = messageStats.get(messageType);
statsBuilder.append("\n ").append(messageType.name())
.append(": count=").append(stats.count.sum())
.append(", total bytes=").append(stats.totalBytes.sum());
});
}
// Minimum version
@@ -827,20 +910,22 @@ public class Peer {
// Common block data
public boolean canUseCachedCommonBlockData() {
PeerChainTipData peerChainTipData = this.getChainTipData();
CommonBlockData commonBlockData = this.getCommonBlockData();
BlockSummaryData peerChainTipData = this.getChainTipData();
if (peerChainTipData == null || peerChainTipData.getSignature() == null)
return false;
if (peerChainTipData != null && commonBlockData != null) {
PeerChainTipData commonBlockChainTipData = commonBlockData.getChainTipData();
if (peerChainTipData.getLastBlockSignature() != null && commonBlockChainTipData != null
&& commonBlockChainTipData.getLastBlockSignature() != null) {
if (Arrays.equals(peerChainTipData.getLastBlockSignature(),
commonBlockChainTipData.getLastBlockSignature())) {
return true;
}
}
}
return false;
CommonBlockData commonBlockData = this.getCommonBlockData();
if (commonBlockData == null)
return false;
BlockSummaryData commonBlockChainTipData = commonBlockData.getChainTipData();
if (commonBlockChainTipData == null || commonBlockChainTipData.getSignature() == null)
return false;
if (!Arrays.equals(peerChainTipData.getSignature(), commonBlockChainTipData.getSignature()))
return false;
return true;
}
@@ -893,6 +978,10 @@ public class Peer {
return maxConnectionAge;
}
public void setMaxConnectionAge(long maxConnectionAge) {
this.maxConnectionAge = maxConnectionAge;
}
public boolean hasReachedMaxConnectionAge() {
return this.getConnectionAge() > this.getMaxConnectionAge();
}

View File

@@ -0,0 +1,70 @@
package org.qortal.network.message;
import com.google.common.primitives.Longs;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.transform.Transformer;
import org.qortal.utils.Base58;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class AccountBalanceMessage extends Message {
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
private AccountBalanceData accountBalanceData;
public AccountBalanceMessage(AccountBalanceData accountBalanceData) {
super(MessageType.ACCOUNT_BALANCE);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
// Send raw address instead of base58 encoded
byte[] address = Base58.decode(accountBalanceData.getAddress());
bytes.write(address);
bytes.write(Longs.toByteArray(accountBalanceData.getAssetId()));
bytes.write(Longs.toByteArray(accountBalanceData.getBalance()));
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
public AccountBalanceMessage(int id, AccountBalanceData accountBalanceData) {
super(id, MessageType.ACCOUNT_BALANCE);
this.accountBalanceData = accountBalanceData;
}
public AccountBalanceData getAccountBalanceData() {
return this.accountBalanceData;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
byte[] addressBytes = new byte[ADDRESS_LENGTH];
byteBuffer.get(addressBytes);
String address = Base58.encode(addressBytes);
long assetId = byteBuffer.getLong();
long balance = byteBuffer.getLong();
AccountBalanceData accountBalanceData = new AccountBalanceData(address, assetId, balance);
return new AccountBalanceMessage(id, accountBalanceData);
}
public AccountBalanceMessage cloneWithNewId(int newId) {
AccountBalanceMessage clone = new AccountBalanceMessage(this.accountBalanceData);
clone.setId(newId);
return clone;
}
}

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