forked from Qortal/qortal
Compare commits
415 Commits
v3.0.4
...
qdn-direct
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaa0b25106 | ||
|
|
1d7203a6fb | ||
|
|
1030b00f0a | ||
|
|
0c16d1fc11 | ||
|
|
ed04375385 | ||
|
|
6e49d20383 | ||
|
|
dc34eed203 | ||
|
|
fbe4f3fad8 | ||
|
|
599877195b | ||
|
|
0695039ee3 | ||
|
|
a4bcd4451c | ||
|
|
e5b4b61832 | ||
|
|
dd55dc277b | ||
|
|
81ef1ae964 | ||
|
|
46701e4de7 | ||
|
|
568497e1c5 | ||
|
|
f3f8e0013d | ||
|
|
d03c145189 | ||
|
|
682a5fde94 | ||
|
|
cca5bac30a | ||
|
|
64e102a8c6 | ||
|
|
df290950ea | ||
|
|
ae64be4802 | ||
|
|
d98678fc5f | ||
|
|
311f41c610 | ||
|
|
0a156c76a2 | ||
|
|
70eaaa9e3b | ||
|
|
3e622f7185 | ||
|
|
3f12be50ac | ||
|
|
68412b49a1 | ||
|
|
c9b2620461 | ||
|
|
337b03aa68 | ||
|
|
df3f16ccf1 | ||
|
|
22aa5c41b5 | ||
|
|
8e09567221 | ||
|
|
3505788d42 | ||
|
|
91e0c9b940 | ||
|
|
00996b047f | ||
|
|
44fc0f367d | ||
|
|
b0e6259073 | ||
|
|
6255b2a907 | ||
|
|
a5fb0be274 | ||
|
|
e835f6d998 | ||
|
|
3d99f86630 | ||
|
|
8d1a58ec06 | ||
|
|
2e5a7cb5a1 | ||
|
|
895f02f178 | ||
|
|
c59869982b | ||
|
|
3b3368f950 | ||
|
|
3f02c760c2 | ||
|
|
fee603e500 | ||
|
|
ad31d8014d | ||
|
|
58a0ac74d2 | ||
|
|
8388aa9c23 | ||
|
|
c1894d8c00 | ||
|
|
f7f9cdc518 | ||
|
|
850d7f8220 | ||
|
|
051043283c | ||
|
|
15bc69de01 | ||
|
|
ee3cfa4d6d | ||
|
|
df1f3079a5 | ||
|
|
d9ae8a5552 | ||
|
|
2326c31ee7 | ||
|
|
91cb0f30dd | ||
|
|
c0307c352c | ||
|
|
8fd7c1b313 | ||
|
|
b8147659b1 | ||
|
|
7a1bac682f | ||
|
|
9fdb7c977f | ||
|
|
4f3948323b | ||
|
|
70fcc1f712 | ||
|
|
f20fe9199f | ||
|
|
91dee4a3b8 | ||
|
|
0b89b8084e | ||
|
|
a5a80302b2 | ||
|
|
e61a24ee7b | ||
|
|
55ed342b59 | ||
|
|
3c6f79eec0 | ||
|
|
590800ac1d | ||
|
|
95c412b946 | ||
|
|
a232395750 | ||
|
|
6edbc8b6a5 | ||
|
|
f8ffeed302 | ||
|
|
e2ee68427c | ||
|
|
74ff23239d | ||
|
|
f1fa2ba2f6 | ||
|
|
e1522cec94 | ||
|
|
8841b3cbb1 | ||
|
|
94260bd93f | ||
|
|
15ff8af7ac | ||
|
|
d420033b36 | ||
|
|
bda63f0310 | ||
|
|
54add26ccb | ||
|
|
089b068362 | ||
|
|
fe474b4507 | ||
|
|
bbe15b563c | ||
|
|
59025b8f47 | ||
|
|
1b42c5edb1 | ||
|
|
362335913d | ||
|
|
4340dac595 | ||
|
|
f3e1fc884c | ||
|
|
39c06d8817 | ||
|
|
91cee36c21 | ||
|
|
6bef883942 | ||
|
|
25ba2406c0 | ||
|
|
e4dc8f85a7 | ||
|
|
12a4a260c8 | ||
|
|
268f02b5c3 | ||
|
|
13eff43b87 | ||
|
|
e604a19bce | ||
|
|
e63e39fe9a | ||
|
|
584c951824 | ||
|
|
f0d9982ee4 | ||
|
|
c65de74d13 | ||
|
|
df0a9701ba | ||
|
|
4ec7b1ff1e | ||
|
|
7d3a465386 | ||
|
|
30347900d9 | ||
|
|
e5f88fe2f4 | ||
|
|
0d0ccfd0ac | ||
|
|
9013d11d24 | ||
|
|
fc5672a161 | ||
|
|
221c3629e4 | ||
|
|
76fc56f1c9 | ||
|
|
8e59aa2885 | ||
|
|
0738dbd613 | ||
|
|
196ecffaf3 | ||
|
|
a0fedbd4b0 | ||
|
|
7c47e22000 | ||
|
|
6aad6a1618 | ||
|
|
b764172500 | ||
|
|
c185d79672 | ||
|
|
76b8ba91dd | ||
|
|
0418c831e6 | ||
|
|
4078f94caa | ||
|
|
a12ae8ad24 | ||
|
|
498ca29aab | ||
|
|
ba70e457b6 | ||
|
|
d62808fe1d | ||
|
|
6c14b79dfb | ||
|
|
631a253bcc | ||
|
|
4cb63100d3 | ||
|
|
42fcee0cfd | ||
|
|
829a2e937b | ||
|
|
5d7e5e8e59 | ||
|
|
6f0a0ef324 | ||
|
|
f7fe91abeb | ||
|
|
7252e8d160 | ||
|
|
2630c35f8c | ||
|
|
49f466c073 | ||
|
|
c198f785e6 | ||
|
|
5be093dafc | ||
|
|
2c33d5256c | ||
|
|
4448e2b5df | ||
|
|
146d234dec | ||
|
|
18d5c924e6 | ||
|
|
b520838195 | ||
|
|
1b036b763c | ||
|
|
8545a8bf0d | ||
|
|
f0136a5018 | ||
|
|
6697b3376b | ||
|
|
ea785f79b8 | ||
|
|
0352a09de7 | ||
|
|
5b4f15ab2e | ||
|
|
fd37c2b76b | ||
|
|
924aa05681 | ||
|
|
84b42210f1 | ||
|
|
941080c395 | ||
|
|
35d9a10cf4 | ||
|
|
7c181379b4 | ||
|
|
f9576d8afb | ||
|
|
6a8a113fa1 | ||
|
|
ef59c34165 | ||
|
|
a19e1f06c0 | ||
|
|
a9371f0a90 | ||
|
|
a7a94e49e8 | ||
|
|
affd100298 | ||
|
|
fd6ec301a4 | ||
|
|
5666e6084b | ||
|
|
69309c437e | ||
|
|
e392e4d344 | ||
|
|
bd53856927 | ||
|
|
cbd1018ecf | ||
|
|
46606152eb | ||
|
|
e6f93e0a08 | ||
|
|
8d81f1822f | ||
|
|
5903607363 | ||
|
|
590a8f52db | ||
|
|
ecac47d1bc | ||
|
|
3b477ef637 | ||
|
|
e2ef5b2ef3 | ||
|
|
1d59feeb72 | ||
|
|
c53dd31765 | ||
|
|
4c02081992 | ||
|
|
cb57af3c53 | ||
|
|
01d810fc00 | ||
|
|
8c2a9279ee | ||
|
|
0d65448f3d | ||
|
|
9da2b3c11a | ||
|
|
95400da977 | ||
|
|
dc41dc4c69 | ||
|
|
a5c11d4c23 | ||
|
|
878394535e | ||
|
|
35dba27a55 | ||
|
|
f22ad13fa9 | ||
|
|
aa2e5cb87b | ||
|
|
7740f3da7e | ||
|
|
badb576991 | ||
|
|
c65a63fc7e | ||
|
|
0111747016 | ||
|
|
eac4b0d87b | ||
|
|
3dadce4da4 | ||
|
|
1864468818 | ||
|
|
1a59379162 | ||
|
|
31d34c3946 | ||
|
|
3cc394f02d | ||
|
|
53c4fe9e80 | ||
|
|
d5521068b0 | ||
|
|
a63ef4010d | ||
|
|
cec3e86eef | ||
|
|
8950bb7af9 | ||
|
|
9e6fe7ceb9 | ||
|
|
c333d18cd0 | ||
|
|
0271ef69c9 | ||
|
|
2d493a4ea2 | ||
|
|
e339ab856f | ||
|
|
782904a971 | ||
|
|
a3753c01bc | ||
|
|
d5c3921846 | ||
|
|
a2c462b3da | ||
|
|
8673c7ef6e | ||
|
|
8d7be7757f | ||
|
|
6b83927048 | ||
|
|
e07adbd60e | ||
|
|
7798b8dcdc | ||
|
|
146e7970bf | ||
|
|
f4f7cc58e3 | ||
|
|
21b4b494e7 | ||
|
|
7307844bee | ||
|
|
5d419dd4ec | ||
|
|
6d0db7cc5e | ||
|
|
8de606588c | ||
|
|
5842b1272d | ||
|
|
35b0a85818 | ||
|
|
fcdd85af6c | ||
|
|
5aac2dc9df | ||
|
|
17a9b4e442 | ||
|
|
becb0b37e6 | ||
|
|
67ca876567 | ||
|
|
464ce66fd5 | ||
|
|
3e505481fe | ||
|
|
c90c3a183e | ||
|
|
d1a7e734dc | ||
|
|
6054982379 | ||
|
|
85b3278c8a | ||
|
|
c90c287601 | ||
|
|
6ee395ed12 | ||
|
|
6275ac2b81 | ||
|
|
fd0a6ec71f | ||
|
|
6c1c814aca | ||
|
|
43791f00aa | ||
|
|
538ac30b4e | ||
|
|
58f11489db | ||
|
|
acddf36467 | ||
|
|
166d32032a | ||
|
|
e4238a62c9 | ||
|
|
ad9c466712 | ||
|
|
a3d31bbaf1 | ||
|
|
4821139501 | ||
|
|
83213800b9 | ||
|
|
265ae19591 | ||
|
|
c1598d20b5 | ||
|
|
0712259057 | ||
|
|
ea42a5617f | ||
|
|
58a690e2c3 | ||
|
|
3ae2f0086e | ||
|
|
19c83cc54d | ||
|
|
8ac298e07d | ||
|
|
9b43e4ea3d | ||
|
|
dbacfb964b | ||
|
|
a664a6a790 | ||
|
|
ee1f072056 | ||
|
|
a6aabaa7f0 | ||
|
|
49b307db60 | ||
|
|
f7341cd9ab | ||
|
|
6932fb9935 | ||
|
|
2343e739d1 | ||
|
|
fc82f0b622 | ||
|
|
c0c50f2e18 | ||
|
|
9332d7207e | ||
|
|
a8c79b807b | ||
|
|
2637311ef5 | ||
|
|
06b5b8f793 | ||
|
|
61f58173cb | ||
|
|
b7b66f6cba | ||
|
|
dda2316884 | ||
|
|
b782679d1f | ||
|
|
b0f19f8f70 | ||
|
|
de5f31ac58 | ||
|
|
214f49e356 | ||
|
|
d7658ee9f9 | ||
|
|
70c864bc2f | ||
|
|
9804eccbf0 | ||
|
|
d1f24d45da | ||
|
|
9630625449 | ||
|
|
b72153f62b | ||
|
|
0a88a0c95e | ||
|
|
ab4ba9bb17 | ||
|
|
a49218a840 | ||
|
|
b6d633ab24 | ||
|
|
133943cd4e | ||
|
|
f8ffb1a179 | ||
|
|
41c4e0c83e | ||
|
|
99f6bb5ac6 | ||
|
|
3e0306f646 | ||
|
|
84e4f9a1c1 | ||
|
|
cd5ce6dd5e | ||
|
|
9ec4e24ef6 | ||
|
|
fa447ccded | ||
|
|
ef838627c4 | ||
|
|
b8aaf14cdc | ||
|
|
2740543abf | ||
|
|
3c526db52e | ||
|
|
cfe0414d96 | ||
|
|
08e06ba11a | ||
|
|
8c03164ea5 | ||
|
|
0fe2f226bc | ||
|
|
55b5702158 | ||
|
|
a4cbbb3868 | ||
|
|
816b01c1fc | ||
|
|
483e7549f8 | ||
|
|
60d71863dc | ||
|
|
170244e679 | ||
|
|
472e1da792 | ||
|
|
cbf03d58c8 | ||
|
|
ba41d84af9 | ||
|
|
98831a9449 | ||
|
|
9692539a3f | ||
|
|
76df332b57 | ||
|
|
c6405340bc | ||
|
|
775e3c065e | ||
|
|
8937b3ec86 | ||
|
|
3fbb86fded | ||
|
|
0cf2f7f254 | ||
|
|
9e571b87e8 | ||
|
|
23bafb6233 | ||
|
|
6dec65c5d9 | ||
|
|
4e59eb8958 | ||
|
|
756d5e685a | ||
|
|
f52530b848 | ||
|
|
c2bf37b878 | ||
|
|
98a2dd04b8 | ||
|
|
694ea689c8 | ||
|
|
618aaaf243 | ||
|
|
9224ffbf73 | ||
|
|
892612c084 | ||
|
|
077165b807 | ||
|
|
7994fc6407 | ||
|
|
d98df3e47d | ||
|
|
1064b1a08b | ||
|
|
0b7a7ed0f1 | ||
|
|
114b1aac76 | ||
|
|
6d06953a0e | ||
|
|
0430fc8a47 | ||
|
|
7338f5f985 | ||
|
|
640bcdd504 | ||
|
|
c9d5d996e5 | ||
|
|
710befec0c | ||
|
|
8ccb158241 | ||
|
|
97199d9b91 | ||
|
|
5a8b895475 | ||
|
|
6c9600cda0 | ||
|
|
82fa6a4fd8 | ||
|
|
45f2d7ab70 | ||
|
|
33731b969a | ||
|
|
40a8cdc71f | ||
|
|
cbe83987d8 | ||
|
|
01e4bf3a77 | ||
|
|
b198a8ea07 | ||
|
|
e2e87766fa | ||
|
|
f005a0975d | ||
|
|
5700369935 | ||
|
|
8a1fb6fe4e | ||
|
|
5b788dad2f | ||
|
|
fa2bd40d5f | ||
|
|
074bfadb28 | ||
|
|
bd60c793be | ||
|
|
90f3d2568a | ||
|
|
c73cdefe6f | ||
|
|
c5093168b1 | ||
|
|
a35e309a2f | ||
|
|
d4ff7bbe4d | ||
|
|
d4d73fc5fc | ||
|
|
bb35030112 | ||
|
|
7aed0354f1 | ||
|
|
c4f763960c | ||
|
|
c5182a4589 | ||
|
|
fc1a376fbd | ||
|
|
27387a134f | ||
|
|
be7bb2df9e | ||
|
|
72a291a54a | ||
|
|
b1342d84fb | ||
|
|
cdd57190ce | ||
|
|
d200a098cd | ||
|
|
a0ed3f53a4 | ||
|
|
a2b2b63932 | ||
|
|
244d4f78e2 | ||
|
|
be561a1609 | ||
|
|
dedf65bd4b | ||
|
|
a79ed02ccf | ||
|
|
79f87babdf | ||
|
|
f296d5138b | ||
|
|
a790b2e529 | ||
|
|
eb9b94b9c6 | ||
|
|
e505067759 |
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM maven:3-openjdk-11 as builder
|
||||
|
||||
WORKDIR /work
|
||||
COPY ./ /work/
|
||||
RUN mvn clean package
|
||||
|
||||
###
|
||||
FROM openjdk:11
|
||||
|
||||
RUN useradd -r -u 1000 -g users qortal && \
|
||||
mkdir /usr/local/qortal /qortal && \
|
||||
chown 1000:100 /qortal
|
||||
|
||||
COPY --from=builder /work/log4j2.properties /usr/local/qortal/
|
||||
COPY --from=builder /work/target/qortal*.jar /usr/local/qortal/qortal.jar
|
||||
|
||||
USER 1000:100
|
||||
|
||||
EXPOSE 12391 12392
|
||||
HEALTHCHECK --start-period=5m CMD curl -sf http://127.0.0.1:12391/admin/info || exit 1
|
||||
|
||||
WORKDIR /qortal
|
||||
VOLUME /qortal
|
||||
|
||||
ENTRYPOINT ["java"]
|
||||
CMD ["-Djava.net.preferIPv4Stack=false", "-jar", "/usr/local/qortal/qortal.jar"]
|
||||
@@ -1,10 +1,4 @@
|
||||
# Qortal Data Node
|
||||
|
||||
## Important
|
||||
|
||||
This code is unfinished, and we haven't had the official genesis block for the data chain yet.
|
||||
Therefore it is only possible to use this code if you first create your own test chain. I would
|
||||
highly recommend waiting until the code is in a more complete state before trying to run this.
|
||||
# Qortal Project - Official Repo
|
||||
|
||||
## Build / run
|
||||
|
||||
|
||||
30
TestNets.md
30
TestNets.md
@@ -41,13 +41,39 @@
|
||||
- Start up at least as many nodes as `minBlockchainPeers` (or adjust this value instead)
|
||||
- Probably best to perform API call `DELETE /peers/known`
|
||||
- Add other nodes via API call `POST /peers <peer-hostname-or-IP>`
|
||||
- Add minting private key to node(s) via API call `POST /admin/mintingaccounts <minting-private-key>`
|
||||
This key must have corresponding `REWARD_SHARE` transaction in testnet genesis block
|
||||
- Add minting private key to nodes via API call `POST /admin/mintingaccounts <minting-private-key>`
|
||||
The keys must have corresponding `REWARD_SHARE` transactions in testnet genesis block
|
||||
- You must have at least 2 separate minting keys and two separate nodes. Assign one minting key to each node.
|
||||
- Alternatively, comment out the `if (mintedLastBlock) { }` conditional in BlockMinter.java to allow for a single node and key.
|
||||
- Wait for genesis block timestamp to pass
|
||||
- A node should mint block 2 approximately 60 seconds after genesis block timestamp
|
||||
- Other testnet nodes will sync *as long as there is at least `minBlockchainPeers` peers with an "up-to-date" chain`
|
||||
- You can also use API call `POST /admin/forcesync <connected-peer-IP-and-port>` on stuck nodes
|
||||
|
||||
## 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
|
||||
|
||||
## Fixed network
|
||||
|
||||
To restrict a testnet to a set of private nodes, you can use the "fixed network" feature.
|
||||
This ensures that the testnet nodes only communicate with each other and not other known peers.
|
||||
To do this, add the following setting to each testnet node, substituting the IP addresses:
|
||||
```
|
||||
"fixedNetwork": [
|
||||
"192.168.0.101:62392",
|
||||
"192.168.0.102:62392",
|
||||
"192.168.0.103:62392"
|
||||
]
|
||||
```
|
||||
|
||||
## Dealing with stuck chain
|
||||
|
||||
Maybe your nodes have been offline and no-one has minted a recent testnet block.
|
||||
|
||||
@@ -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:{1FB9DC61-308D-4726-B993-82B2D51AD453} 1049:{0EAB0862-4F57-4D79-B8B4-C3E1D84484A2} 2052:{28F67872-0D8B-48D1-9DE1-7F11D5919CB8} 2057:{725A8624-CB8A-426D-8162-68980ED45BCB} " Type="16"/>
|
||||
<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="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="3.0.1" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="3.2.5" Type="32"/>
|
||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
@@ -212,7 +212,7 @@
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{163A0AFB-3694-4E1B-ABB8-7C5F28F61305}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{D5A1DC7D-914F-4425-8BA6-A1AE05D0F361}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
||||
@@ -1173,7 +1173,7 @@
|
||||
<ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
|
||||
<ROW Action="AI_SetPermissions" Type="11265" Source="userAccounts.dll" Target="OnSetPermissions" WithoutSeq="true"/>
|
||||
<ROW Action="CustomizeLog4j2PropertiesScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Make copy fso.CopyFile(appDir + "log4j2.properties", appDir + "log4j2-orig.properties", true); // overwrite // Rewrite %AppDir%\log4j2.properties to update logfile storage path var fin = fso.OpenTextFile(appDir + "log4j2-orig.properties", ForReading, false); // no create var fout = fso.OpenTextFile(appDir + "log4j2.properties", ForWriting, true); // can create // Copy lines with rewriting where necessary while( !fin.AtEndOfStream ) { 	var line = fin.ReadLine(); 	var start = line.indexOf("property.dirname"); 	if (start > 0) { 		// line: # property.dirname = ...appdata... 		// uncomment/replace this line for Windows 		fout.WriteLine( "property.dirname = " + dataFolder.split('\\').join('\\\\') ); 	} else { 		// not found - output verbatim 		fout.WriteLine( line ); 	} } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_4"/>
|
||||
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder var fts = fso.OpenTextFile(appDir + "settings.json", ForWriting, true); fts.WriteLine( "{" ); // We need to escape Windows path backslashes to keep JSON valid fts.WriteLine( " \"userPath\": \"" + dataFolder.split('\\').join('\\\\') + "\"" ); fts.WriteLine( "}" ); fts.Close(); // Make copy fso.CopyFile(dataFolder + "settings.json", dataFolder + "settings-orig.json", true); // overwrite // Rewrite settings.json to update repository path var fin = fso.OpenTextFile(dataFolder + "settings-orig.json", ForReading, false); var fout = fso.OpenTextFile(dataFolder + "settings.json", ForWriting, true); // First line should contain opening brace fout.WriteLine( fin.ReadLine() ); // Append our entries fout.WriteLine( " \"repositoryPath\": \"" + dataFolder.split('\\').join('\\\\') + "db\"," ); // copy rest of settings while( !fin.AtEndOfStream ) { 	fout.WriteLine( fin.ReadLine() ); } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_3"/>
|
||||
<ROW Action="CustomizeSettingsJsonScript" Type="3109" Target="Script Text" TargetUnformatted="var actionData = Session.Property("CustomActionData"); var actionDataArray = actionData.split("|"); var appDir = actionDataArray[0]; var dataFolder = actionDataArray[1] + actionDataArray[2] + "\\"; var ForReading = 1, ForWriting = 2, ForAppending = 8; var fso = new ActiveXObject("Scripting.FileSystemObject"); // Create basic %APPDIR%\settings.json with path to real settings.json in dataFolder var fts = fso.OpenTextFile(appDir + "settings.json", ForWriting, true); fts.WriteLine( "{" ); // We need to escape Windows path backslashes to keep JSON valid fts.WriteLine( " \"userPath\": \"" + dataFolder.split('\\').join('\\\\') + "\"" ); fts.WriteLine( "}" ); fts.Close(); // Make copy fso.CopyFile(dataFolder + "settings.json", dataFolder + "settings-orig.json", true); // overwrite // Rewrite settings.json to update repository path var fin = fso.OpenTextFile(dataFolder + "settings-orig.json", ForReading, false); var fout = fso.OpenTextFile(dataFolder + "settings.json", ForWriting, true); // First line should contain opening brace fout.WriteLine( fin.ReadLine() ); // Append our entries fout.WriteLine( " \"repositoryPath\": \"" + dataFolder.split('\\').join('\\\\') + "db\"," ); fout.WriteLine( " \"dataPath\": \"" + dataFolder.split('\\').join('\\\\') + "data\"," ); // copy rest of settings while( !fin.AtEndOfStream ) { 	fout.WriteLine( fin.ReadLine() ); } fin.Close(); fout.Close(); " AdditionalSeq="AI_DATA_SETTER_3"/>
|
||||
<ROW Action="DetectRunningProcess" Type="1" Source="aicustact.dll" Target="DetectProcess" Options="3" AdditionalSeq="AI_DATA_SETTER_8"/>
|
||||
<ROW Action="DetectW32Time" Type="1" Source="aicustact.dll" Target="DetectService" Options="3" AdditionalSeq="AI_DATA_SETTER_11"/>
|
||||
<ROW Action="NTP_config" Type="3090" Source="ntpcfg.bat"/>
|
||||
|
||||
BIN
lib/com/dosse/WaifUPnP/1.1/WaifUPnP-1.1.jar
Normal file
BIN
lib/com/dosse/WaifUPnP/1.1/WaifUPnP-1.1.jar
Normal file
Binary file not shown.
9
lib/com/dosse/WaifUPnP/1.1/WaifUPnP-1.1.pom
Normal file
9
lib/com/dosse/WaifUPnP/1.1/WaifUPnP-1.1.pom
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.dosse</groupId>
|
||||
<artifactId>WaifUPnP</artifactId>
|
||||
<version>1.1</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
||||
12
lib/com/dosse/WaifUPnP/maven-metadata-local.xml
Normal file
12
lib/com/dosse/WaifUPnP/maven-metadata-local.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>com.dosse</groupId>
|
||||
<artifactId>WaifUPnP</artifactId>
|
||||
<versioning>
|
||||
<release>1.1</release>
|
||||
<versions>
|
||||
<version>1.1</version>
|
||||
</versions>
|
||||
<lastUpdated>20220218200127</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
24
pom.xml
24
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.0.4</version>
|
||||
<version>3.2.5</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
@@ -21,6 +21,9 @@
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
<guava.version>28.1-jre</guava.version>
|
||||
<hsqldb.version>2.5.1</hsqldb.version>
|
||||
<homoglyph.version>1.2.1</homoglyph.version>
|
||||
<icu4j.version>70.1</icu4j.version>
|
||||
<upnp.version>1.1</upnp.version>
|
||||
<jersey.version>2.29.1</jersey.version>
|
||||
<jetty.version>9.4.29.v20200521</jetty.version>
|
||||
<log4j.version>2.17.1</log4j.version>
|
||||
@@ -427,6 +430,12 @@
|
||||
<artifactId>AT</artifactId>
|
||||
<version>${ciyam-at.version}</version>
|
||||
</dependency>
|
||||
<!-- UPnP support -->
|
||||
<dependency>
|
||||
<groupId>com.dosse</groupId>
|
||||
<artifactId>WaifUPnP</artifactId>
|
||||
<version>${upnp.version}</version>
|
||||
</dependency>
|
||||
<!-- Bitcoin support -->
|
||||
<dependency>
|
||||
<groupId>org.bitcoinj</groupId>
|
||||
@@ -561,7 +570,18 @@
|
||||
<dependency>
|
||||
<groupId>net.codebox</groupId>
|
||||
<artifactId>homoglyph</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<version>${homoglyph.version}</version>
|
||||
</dependency>
|
||||
<!-- Unicode support -->
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j</artifactId>
|
||||
<version>${icu4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j-charset</artifactId>
|
||||
<version>${icu4j.version}</version>
|
||||
</dependency>
|
||||
<!-- Jetty -->
|
||||
<dependency>
|
||||
|
||||
@@ -205,6 +205,12 @@ public class Account {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns account's blockMinted (0+) or null if account not found in repository. */
|
||||
public Integer getBlocksMinted() throws DataException {
|
||||
return this.repository.getAccountRepository().getMintedBlockCount(this.address);
|
||||
}
|
||||
|
||||
|
||||
/** Returns whether account can build reward-shares.
|
||||
* <p>
|
||||
* To be able to create reward-shares, the account needs to pass at least one of these tests:<br>
|
||||
@@ -272,7 +278,7 @@ public class Account {
|
||||
/**
|
||||
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
||||
* <p>
|
||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
* this is being used on src/main/java/org/qortal/api/resource/AddressesResource.java to fulfil the online accounts api call
|
||||
*
|
||||
* @param repository
|
||||
* @param rewardSharePublicKey
|
||||
@@ -288,5 +294,26 @@ public class Account {
|
||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||
return rewardShareMinter.getEffectiveMintingLevel();
|
||||
}
|
||||
/**
|
||||
* Returns 'effective' minting level, with a fix for the zero level.
|
||||
* <p>
|
||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||
*
|
||||
* @param repository
|
||||
* @param rewardSharePublicKey
|
||||
* @return 0+
|
||||
* @throws DataException
|
||||
*/
|
||||
public static int getRewardShareEffectiveMintingLevelIncludingLevelZero(Repository repository, byte[] rewardSharePublicKey) throws DataException {
|
||||
// Find actual minter and get their effective minting level
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey);
|
||||
if (rewardShareData == null)
|
||||
return 0;
|
||||
|
||||
else if(!rewardShareData.getMinter().equals(rewardShareData.getRecipient()))//the minter is different than the recipient this means sponsorship
|
||||
return 0;
|
||||
|
||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||
return rewardShareMinter.getEffectiveMintingLevel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,13 +40,7 @@ import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
import org.qortal.api.websocket.ActiveChatsWebSocket;
|
||||
import org.qortal.api.websocket.AdminStatusWebSocket;
|
||||
import org.qortal.api.websocket.BlocksWebSocket;
|
||||
import org.qortal.api.websocket.ChatMessagesWebSocket;
|
||||
import org.qortal.api.websocket.PresenceWebSocket;
|
||||
import org.qortal.api.websocket.TradeBotWebSocket;
|
||||
import org.qortal.api.websocket.TradeOffersWebSocket;
|
||||
import org.qortal.api.websocket.*;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class ApiService {
|
||||
@@ -212,6 +206,9 @@ public class ApiService {
|
||||
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
|
||||
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
|
||||
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
|
||||
context.addServlet(TradePresenceWebSocket.class, "/websockets/crosschain/tradepresence");
|
||||
|
||||
// Deprecated
|
||||
context.addServlet(PresenceWebSocket.class, "/websockets/presence");
|
||||
|
||||
// Start server
|
||||
|
||||
@@ -28,6 +28,11 @@ public class HTMLParser {
|
||||
// Add base href tag
|
||||
String baseElement = String.format("<base href=\"%s\">", baseUrl);
|
||||
head.get(0).prepend(baseElement);
|
||||
|
||||
// Add meta charset tag
|
||||
String metaCharsetElement = "<meta charset=\"UTF-8\">";
|
||||
head.get(0).prepend(metaCharsetElement);
|
||||
|
||||
}
|
||||
String html = document.html();
|
||||
this.data = html.getBytes();
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.Map;
|
||||
|
||||
|
||||
@Path("/")
|
||||
@Tag(name = "Gateway")
|
||||
@Tag(name = "Domain Map")
|
||||
public class DomainMapResource {
|
||||
|
||||
@Context HttpServletRequest request;
|
||||
|
||||
@@ -65,7 +65,7 @@ public class GatewayResource {
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus();
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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.Synchronizer;
|
||||
import org.qortal.network.Network;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@@ -22,10 +23,10 @@ public class NodeStatus {
|
||||
public NodeStatus() {
|
||||
this.isMintingPossible = Controller.getInstance().isMintingPossible();
|
||||
|
||||
this.syncPercent = Controller.getInstance().getSyncPercent();
|
||||
this.isSynchronizing = this.syncPercent != null;
|
||||
this.syncPercent = Synchronizer.getInstance().getSyncPercent();
|
||||
this.isSynchronizing = Synchronizer.getInstance().isSynchronizing();
|
||||
|
||||
this.numberOfConnections = Network.getInstance().getHandshakedPeers().size();
|
||||
this.numberOfConnections = Network.getInstance().getImmutableHandshakedPeers().size();
|
||||
|
||||
this.height = Controller.getInstance().getChainHeight();
|
||||
}
|
||||
|
||||
@@ -30,7 +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.Controller;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
@@ -156,7 +156,7 @@ public class AddressesResource {
|
||||
)
|
||||
@ApiErrors({ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
|
||||
public List<ApiOnlineAccount> getOnlineAccounts() {
|
||||
List<OnlineAccountData> onlineAccounts = Controller.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
|
||||
// Map OnlineAccountData entries to OnlineAccount via reward-share data
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -191,14 +191,14 @@ public class AddressesResource {
|
||||
)
|
||||
@ApiErrors({ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
|
||||
public List<OnlineAccountLevel> getOnlineAccountsByLevel() {
|
||||
List<OnlineAccountData> onlineAccounts = Controller.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<OnlineAccountLevel> onlineAccountLevels = new ArrayList<>();
|
||||
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
try {
|
||||
final int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, onlineAccountData.getPublicKey());
|
||||
final int minterLevel = Account.getRewardShareEffectiveMintingLevelIncludingLevelZero(repository, onlineAccountData.getPublicKey());
|
||||
|
||||
OnlineAccountLevel onlineAccountLevel = onlineAccountLevels.stream()
|
||||
.filter(a -> a.getLevel() == minterLevel)
|
||||
|
||||
@@ -35,7 +35,6 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.appender.RollingFileAppender;
|
||||
import org.checkerframework.checker.units.qual.A;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.*;
|
||||
@@ -44,6 +43,7 @@ import org.qortal.api.model.NodeInfo;
|
||||
import org.qortal.api.model.NodeStatus;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
@@ -314,6 +314,7 @@ public class AdminResource {
|
||||
|
||||
repository.getAccountRepository().save(mintingAccountData);
|
||||
repository.saveChanges();
|
||||
repository.exportNodeLocalData();//after adding new minting account let's persist it to the backup MintingAccounts.json
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY, e);
|
||||
} catch (DataException e) {
|
||||
@@ -354,6 +355,7 @@ public class AdminResource {
|
||||
return "false";
|
||||
|
||||
repository.saveChanges();
|
||||
repository.exportNodeLocalData();//after removing new minting account let's persist it to the backup MintingAccounts.json
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY, e);
|
||||
} catch (DataException e) {
|
||||
@@ -379,6 +381,10 @@ public class AdminResource {
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
) @QueryParam("offset") Integer offset, @Parameter(
|
||||
name = "tail",
|
||||
description = "Fetch most recent log lines",
|
||||
schema = @Schema(type = "boolean")
|
||||
) @QueryParam("tail") Boolean tail, @Parameter(
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
LoggerContext loggerContext = (LoggerContext) LogManager.getContext();
|
||||
@@ -394,6 +400,13 @@ public class AdminResource {
|
||||
if (reverse != null && reverse)
|
||||
logLines = Lists.reverse(logLines);
|
||||
|
||||
// Tail mode - return the last X lines (where X = limit)
|
||||
if (tail != null && tail) {
|
||||
if (limit != null && limit > 0) {
|
||||
offset = logLines.size() - limit;
|
||||
}
|
||||
}
|
||||
|
||||
// offset out of bounds?
|
||||
if (offset != null && (offset < 0 || offset >= logLines.size()))
|
||||
return "";
|
||||
@@ -414,7 +427,7 @@ public class AdminResource {
|
||||
|
||||
limit = Math.min(limit, logLines.size());
|
||||
|
||||
logLines.subList(limit - 1, logLines.size()).clear();
|
||||
logLines.subList(limit, logLines.size()).clear();
|
||||
|
||||
return String.join("\n", logLines);
|
||||
} catch (IOException e) {
|
||||
@@ -511,7 +524,7 @@ public class AdminResource {
|
||||
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress);
|
||||
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress();
|
||||
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
List<Peer> peers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().equals(resolvedAddress)).findFirst().orElse(null);
|
||||
|
||||
if (targetPeer == null)
|
||||
@@ -525,7 +538,7 @@ public class AdminResource {
|
||||
SynchronizationResult syncResult;
|
||||
try {
|
||||
do {
|
||||
syncResult = Controller.getInstance().actuallySynchronize(targetPeer, true);
|
||||
syncResult = Synchronizer.getInstance().actuallySynchronize(targetPeer, true);
|
||||
} while (syncResult == SynchronizationResult.OK);
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
@@ -545,7 +558,7 @@ public class AdminResource {
|
||||
@Path("/repository/data")
|
||||
@Operation(
|
||||
summary = "Export sensitive/node-local data from repository.",
|
||||
description = "Exports data to .script files on local machine"
|
||||
description = "Exports data to .json files on local machine"
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_DATA, ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
@@ -586,10 +599,6 @@ public class AdminResource {
|
||||
public String importRepository(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String filename) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
// Hard-coded because it's too dangerous to allow user-supplied filenames in weaker security contexts
|
||||
if (Settings.getInstance().getApiKey() == null)
|
||||
filename = "qortal-backup/TradeBotStates.json";
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -33,13 +34,14 @@ import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.arbitrary.*;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryMetadataManager;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceNameInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.arbitrary.*;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -88,7 +90,8 @@ 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 = "Include status") @QueryParam("includestatus") Boolean includeStatus) {
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
@@ -110,9 +113,12 @@ public class ArbitraryResource {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus == true) {
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
|
||||
@@ -140,7 +146,8 @@ 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 = "Include status") @QueryParam("includestatus") Boolean includeStatus) {
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
@@ -153,9 +160,12 @@ public class ArbitraryResource {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus == true) {
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
|
||||
@@ -182,7 +192,8 @@ 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 = "Include status") @QueryParam("includestatus") Boolean includeStatus) {
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
@@ -206,9 +217,13 @@ public class ArbitraryResource {
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.getArbitraryResources(service, identifier, name, defaultRes, null, null, reverse);
|
||||
|
||||
if (includeStatus != null && includeStatus == true) {
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
creatorName.resources = resources;
|
||||
}
|
||||
}
|
||||
@@ -232,9 +247,10 @@ public class ArbitraryResource {
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ArbitraryResourceStatus getDefaultResourceStatus(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("build") Boolean build) {
|
||||
public ArbitraryResourceStatus getDefaultResourceStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
|
||||
return this.getStatus(service, name, null, build);
|
||||
@@ -252,10 +268,11 @@ public class ArbitraryResource {
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ArbitraryResourceStatus getResourceStatus(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("build") Boolean build) {
|
||||
public ArbitraryResourceStatus getResourceStatus(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
|
||||
return this.getStatus(service, name, identifier, build);
|
||||
@@ -388,6 +405,28 @@ public class ArbitraryResource {
|
||||
return Settings.getInstance().isRelayModeEnabled();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/categories")
|
||||
@Operation(
|
||||
summary = "List arbitrary transaction categories",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryCategoryInfo.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<ArbitraryCategoryInfo> getCategories() {
|
||||
List<ArbitraryCategoryInfo> categories = new ArrayList<>();
|
||||
for (Category category : Category.values()) {
|
||||
ArbitraryCategoryInfo arbitraryCategory = new ArbitraryCategoryInfo();
|
||||
arbitraryCategory.id = category.toString();
|
||||
arbitraryCategory.name = category.getName();
|
||||
categories.add(arbitraryCategory);
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/hosted/transactions")
|
||||
@Operation(
|
||||
@@ -429,15 +468,24 @@ public class ArbitraryResource {
|
||||
public List<ArbitraryResourceInfo> getHostedResources(
|
||||
@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset) {
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@QueryParam("query") String query) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
List<ArbitraryResourceInfo> resources = new ArrayList<>();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList;
|
||||
|
||||
if (query == null || query.equals("")) {
|
||||
transactionDataList = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, limit, offset);
|
||||
} else {
|
||||
transactionDataList = ArbitraryDataStorageManager.getInstance().searchHostedTransactions(repository,query, limit, offset);
|
||||
}
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, limit, offset);
|
||||
for (ArbitraryTransactionData transactionData : transactionDataList) {
|
||||
ArbitraryResourceInfo arbitraryResourceInfo = new ArbitraryResourceInfo();
|
||||
arbitraryResourceInfo.name = transactionData.getName();
|
||||
@@ -448,9 +496,12 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus == true) {
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
|
||||
@@ -459,6 +510,8 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@DELETE
|
||||
@Path("/resource/{service}/{name}/{identifier}")
|
||||
@Operation(
|
||||
@@ -574,14 +627,16 @@ public class ArbitraryResource {
|
||||
@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("filepath") String filepath,
|
||||
@QueryParam("rebuild") boolean rebuild) {
|
||||
@QueryParam("rebuild") boolean rebuild,
|
||||
@QueryParam("async") boolean async,
|
||||
@QueryParam("attempts") Integer attempts) {
|
||||
|
||||
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
}
|
||||
|
||||
return this.download(service, name, null, filepath, rebuild);
|
||||
return this.download(service, name, null, filepath, rebuild, async, attempts);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -607,14 +662,64 @@ public class ArbitraryResource {
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("filepath") String filepath,
|
||||
@QueryParam("rebuild") boolean rebuild) {
|
||||
@QueryParam("rebuild") boolean rebuild,
|
||||
@QueryParam("async") boolean async,
|
||||
@QueryParam("attempts") Integer attempts) {
|
||||
|
||||
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
}
|
||||
|
||||
return this.download(service, name, identifier, filepath, rebuild);
|
||||
return this.download(service, name, identifier, filepath, rebuild, async, attempts);
|
||||
}
|
||||
|
||||
|
||||
// Metadata
|
||||
|
||||
@GET
|
||||
@Path("/metadata/{service}/{name}/{identifier}")
|
||||
@Operation(
|
||||
summary = "Fetch raw metadata from resource with supplied service, name, identifier, and relative path",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "Path to file structure containing requested data",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = ArbitraryDataTransactionMetadata.class
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ArbitraryResourceMetadata getMetadata(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
|
||||
try {
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = ArbitraryMetadataManager.getInstance().fetchMetadata(resource, false);
|
||||
if (transactionMetadata != null) {
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
||||
if (resourceMetadata != null) {
|
||||
return resourceMetadata;
|
||||
}
|
||||
else {
|
||||
// The metadata file doesn't contain title, description, category, or tags
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// No metadata exists for this resource
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
|
||||
|
||||
@@ -650,6 +755,10 @@ public class ArbitraryResource {
|
||||
public String post(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
String path) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -657,7 +766,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Path not supplied");
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, path, null, null, false);
|
||||
return this.upload(Service.valueOf(serviceString), name, null, path, null, null, false,
|
||||
title, description, tags, category);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -690,6 +800,10 @@ public class ArbitraryResource {
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
String path) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -697,7 +811,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Path not supplied");
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, path, null, null, false);
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, path, null, null, false,
|
||||
title, description, tags, category);
|
||||
}
|
||||
|
||||
|
||||
@@ -731,6 +846,10 @@ public class ArbitraryResource {
|
||||
public String postBase64EncodedData(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
String base64) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -738,7 +857,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data not supplied");
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64, false);
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64, false,
|
||||
title, description, tags, category);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -769,6 +889,10 @@ public class ArbitraryResource {
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
String base64) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -776,7 +900,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data not supplied");
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64, false);
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64, false,
|
||||
title, description, tags, category);
|
||||
}
|
||||
|
||||
|
||||
@@ -809,6 +934,10 @@ public class ArbitraryResource {
|
||||
public String postZippedData(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
String base64Zip) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -816,7 +945,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data not supplied");
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64Zip, true);
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64Zip, true,
|
||||
title, description, tags, category);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -847,6 +977,10 @@ public class ArbitraryResource {
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
String base64Zip) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -854,7 +988,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data not supplied");
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64Zip, true);
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64Zip, true,
|
||||
title, description, tags, category);
|
||||
}
|
||||
|
||||
|
||||
@@ -890,6 +1025,10 @@ public class ArbitraryResource {
|
||||
public String postString(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
String string) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -897,7 +1036,8 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data string not supplied");
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, string, null, false);
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, string, null, false,
|
||||
title, description, tags, category);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -930,6 +1070,10 @@ public class ArbitraryResource {
|
||||
@PathParam("service") String serviceString,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("title") String title,
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
String string) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -937,13 +1081,16 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data string not supplied");
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, string, null, false);
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, string, null, false,
|
||||
title, description, tags, category);
|
||||
}
|
||||
|
||||
|
||||
// Shared methods
|
||||
|
||||
private String upload(Service service, String name, String identifier, String path, String string, String base64, boolean zipped) {
|
||||
private String upload(Service service, String name, String identifier,
|
||||
String path, String string, String base64, boolean zipped,
|
||||
String title, String description, List<String> tags, Category category) {
|
||||
// Fetch public key from registered name
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
NameData nameData = repository.getNameRepository().fromName(name);
|
||||
@@ -1007,7 +1154,8 @@ public class ArbitraryResource {
|
||||
|
||||
try {
|
||||
ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder(
|
||||
repository, publicKey58, Paths.get(path), name, null, service, identifier
|
||||
repository, publicKey58, Paths.get(path), name, null, service, identifier,
|
||||
title, description, tags, category
|
||||
);
|
||||
|
||||
transactionBuilder.build();
|
||||
@@ -1025,30 +1173,45 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServletResponse download(Service service, String name, String identifier, String filepath, boolean rebuild) {
|
||||
private HttpServletResponse download(Service service, String name, String identifier, String filepath, boolean rebuild, boolean async, Integer maxAttempts) {
|
||||
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
try {
|
||||
|
||||
int attempts = 0;
|
||||
if (maxAttempts == null) {
|
||||
maxAttempts = 5;
|
||||
}
|
||||
|
||||
// Loop until we have data
|
||||
while (!Controller.isStopping()) {
|
||||
attempts++;
|
||||
if (!arbitraryDataReader.isBuilding()) {
|
||||
try {
|
||||
arbitraryDataReader.loadSynchronously(rebuild);
|
||||
break;
|
||||
} catch (MissingDataException e) {
|
||||
if (attempts > 5) {
|
||||
// Give up after 5 attempts
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data unavailable. Please try again later.");
|
||||
if (async) {
|
||||
// Asynchronous
|
||||
arbitraryDataReader.loadAsynchronously(false, 1);
|
||||
}
|
||||
else {
|
||||
// Synchronous
|
||||
while (!Controller.isStopping()) {
|
||||
attempts++;
|
||||
if (!arbitraryDataReader.isBuilding()) {
|
||||
try {
|
||||
arbitraryDataReader.loadSynchronously(rebuild);
|
||||
break;
|
||||
} catch (MissingDataException e) {
|
||||
if (attempts > maxAttempts) {
|
||||
// Give up after 5 attempts
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Data unavailable. Please try again later.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Thread.sleep(3000L);
|
||||
}
|
||||
Thread.sleep(3000L);
|
||||
}
|
||||
|
||||
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
||||
if (outputPath == null) {
|
||||
// Assume the resource doesn't exist
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, "File not found");
|
||||
}
|
||||
|
||||
if (filepath == null || filepath.isEmpty()) {
|
||||
// No file path supplied - so check if this is a single file resource
|
||||
@@ -1097,18 +1260,40 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.getStatus();
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
try {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryResourceStatus status = resource.getStatus(true);
|
||||
if (status != null) {
|
||||
resourceInfo.status = status;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Catch and log all exceptions, since some systems are experiencing 500 errors when including statuses
|
||||
LOGGER.info("Caught exception when adding status to resource %s: %s", resourceInfo, e.toString());
|
||||
}
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
private List<ArbitraryResourceInfo> addMetadataToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Add metadata fields to each resource if they exist
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryResourceStatus status = resource.getStatus();
|
||||
if (status != null) {
|
||||
resourceInfo.status = status;
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata);
|
||||
if (resourceMetadata != null) {
|
||||
resourceInfo.metadata = resourceMetadata;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
}
|
||||
|
||||
@@ -67,11 +67,16 @@ public class CrossChainBitcoinResource {
|
||||
if (!bitcoin.isValidDeterministicKey(key58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
Long balance = bitcoin.getWalletBalance(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
try {
|
||||
Long balance = bitcoin.getWalletBalanceFromTransactions(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
return balance.toString();
|
||||
return balance.toString();
|
||||
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -117,7 +122,7 @@ public class CrossChainBitcoinResource {
|
||||
@Path("/send")
|
||||
@Operation(
|
||||
summary = "Sends BTC from hierarchical, deterministic BIP32 wallet to specific address",
|
||||
description = "Currently only supports 'legacy' P2PKH Bitcoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
|
||||
description = "Currently supports 'legacy' P2PKH Bitcoin 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(
|
||||
|
||||
@@ -65,11 +65,16 @@ public class CrossChainDogecoinResource {
|
||||
if (!dogecoin.isValidDeterministicKey(key58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
Long balance = dogecoin.getWalletBalance(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
try {
|
||||
Long balance = dogecoin.getWalletBalanceFromTransactions(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
return balance.toString();
|
||||
return balance.toString();
|
||||
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
|
||||
@@ -67,11 +67,16 @@ public class CrossChainLitecoinResource {
|
||||
if (!litecoin.isValidDeterministicKey(key58))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
Long balance = litecoin.getWalletBalance(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
try {
|
||||
Long balance = litecoin.getWalletBalanceFromTransactions(key58);
|
||||
if (balance == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
|
||||
return balance.toString();
|
||||
return balance.toString();
|
||||
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -117,7 +122,7 @@ public class CrossChainLitecoinResource {
|
||||
@Path("/send")
|
||||
@Operation(
|
||||
summary = "Sends LTC from hierarchical, deterministic BIP32 wallet to specific address",
|
||||
description = "Currently only supports 'legacy' P2PKH Litecoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
|
||||
description = "Currently supports 'legacy' P2PKH Litecoin 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(
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.CrossChainCancelRequest;
|
||||
import org.qortal.api.model.CrossChainTradeSummary;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
@@ -120,6 +121,8 @@ public class CrossChainResource {
|
||||
crossChainTrades = crossChainTrades.subList(0, upperLimit);
|
||||
}
|
||||
|
||||
crossChainTrades.stream().forEach(CrossChainResource::decorateTradeDataWithPresence);
|
||||
|
||||
return crossChainTrades;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
@@ -151,7 +154,11 @@ public class CrossChainResource {
|
||||
if (acct == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
return acct.populateTradeData(repository, atData);
|
||||
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
|
||||
|
||||
decorateTradeDataWithPresence(crossChainTradeData);
|
||||
|
||||
return crossChainTradeData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -486,4 +493,7 @@ public class CrossChainResource {
|
||||
}
|
||||
}
|
||||
|
||||
private static void decorateTradeDataWithPresence(CrossChainTradeData crossChainTradeData) {
|
||||
TradeBot.getInstance().decorateTradeDataWithPresence(crossChainTradeData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,15 @@ public class GroupsResource {
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getGroupRepository().getAllGroups(limit, offset, reverse);
|
||||
List<GroupData> allGroupData = repository.getGroupRepository().getAllGroups(limit, offset, reverse);
|
||||
allGroupData.forEach(groupData -> {
|
||||
try {
|
||||
groupData.memberCount = repository.getGroupRepository().countGroupMembers(groupData.getGroupId());
|
||||
} catch (DataException e) {
|
||||
// Exclude memberCount for this group
|
||||
}
|
||||
});
|
||||
return allGroupData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -150,7 +158,15 @@ public class GroupsResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getGroupRepository().getGroupsWithMember(member);
|
||||
List<GroupData> allGroupData = repository.getGroupRepository().getGroupsWithMember(member);
|
||||
allGroupData.forEach(groupData -> {
|
||||
try {
|
||||
groupData.memberCount = repository.getGroupRepository().countGroupMembers(groupData.getGroupId());
|
||||
} catch (DataException e) {
|
||||
// Exclude memberCount for this group
|
||||
}
|
||||
});
|
||||
return allGroupData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -177,6 +193,7 @@ public class GroupsResource {
|
||||
if (groupData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.GROUP_UNKNOWN);
|
||||
|
||||
groupData.memberCount = repository.getGroupRepository().countGroupMembers(groupId);
|
||||
return groupData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
@@ -922,4 +939,4 @@ public class GroupsResource {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.model.ConnectedPeer;
|
||||
import org.qortal.api.model.PeersSummary;
|
||||
@@ -61,7 +66,7 @@ public class PeersResource {
|
||||
}
|
||||
)
|
||||
public List<ConnectedPeer> getPeers() {
|
||||
return Network.getInstance().getConnectedPeers().stream().map(ConnectedPeer::new).collect(Collectors.toList());
|
||||
return Network.getInstance().getImmutableConnectedPeers().stream().map(ConnectedPeer::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -127,9 +132,29 @@ public class PeersResource {
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public ExecuteProduceConsume.StatsSnapshot getEngineStats(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
public ExecuteProduceConsume.StatsSnapshot getEngineStats(@HeaderParam(Security.API_KEY_HEADER) String apiKey, @QueryParam("newLoggingLevel") Level newLoggingLevel) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
if (newLoggingLevel != null) {
|
||||
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
|
||||
final Configuration config = ctx.getConfiguration();
|
||||
|
||||
String epcClassName = "org.qortal.network.Network.NetworkProcessor";
|
||||
LoggerConfig loggerConfig = config.getLoggerConfig(epcClassName);
|
||||
LoggerConfig specificConfig = loggerConfig;
|
||||
|
||||
// We need a specific configuration for this logger,
|
||||
// otherwise we would change the level of all other loggers
|
||||
// having the original configuration as parent as well
|
||||
if (!loggerConfig.getName().equals(epcClassName)) {
|
||||
specificConfig = new LoggerConfig(epcClassName, newLoggingLevel, true);
|
||||
specificConfig.setParent(loggerConfig);
|
||||
config.addLogger(epcClassName, specificConfig);
|
||||
}
|
||||
specificConfig.setLevel(newLoggingLevel);
|
||||
ctx.updateLoggers();
|
||||
}
|
||||
|
||||
return Network.getInstance().getStatsSnapshot();
|
||||
}
|
||||
|
||||
@@ -304,7 +329,7 @@ public class PeersResource {
|
||||
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress);
|
||||
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress();
|
||||
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
List<Peer> peers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().equals(resolvedAddress)).findFirst().orElse(null);
|
||||
|
||||
if (targetPeer == null)
|
||||
@@ -352,9 +377,9 @@ public class PeersResource {
|
||||
public PeersSummary peersSummary() {
|
||||
PeersSummary peersSummary = new PeersSummary();
|
||||
|
||||
List<Peer> connectedPeers = Network.getInstance().getConnectedPeers().stream().collect(Collectors.toList());
|
||||
List<Peer> connectedPeers = Network.getInstance().getImmutableConnectedPeers().stream().collect(Collectors.toList());
|
||||
for (Peer peer : connectedPeers) {
|
||||
if (peer.isOutbound()) {
|
||||
if (!peer.isOutbound()) {
|
||||
peersSummary.inboundConnections++;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -74,7 +74,9 @@ public class RenderResource {
|
||||
Method method = Method.PUT;
|
||||
Compression compression = Compression.ZIP;
|
||||
|
||||
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(directoryPath), null, Service.WEBSITE, null, method, compression);
|
||||
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(directoryPath),
|
||||
null, Service.WEBSITE, null, method, compression,
|
||||
null, null, null, null);
|
||||
try {
|
||||
arbitraryDataWriter.save();
|
||||
} catch (IOException | DataException | InterruptedException | MissingDataException e) {
|
||||
@@ -136,34 +138,38 @@ public class RenderResource {
|
||||
@GET
|
||||
@Path("/signature/{signature}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature) {
|
||||
public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, "/", null, "/render/signature", true, true);
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, "/", null, "/render/signature", true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/signature/{signature}/{path:.*}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getPathBySignature(@PathParam("signature") String signature, @PathParam("path") String inPath) {
|
||||
public HttpServletResponse getPathBySignature(@PathParam("signature") String signature, @PathParam("path") String inPath,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, inPath,null, "/render/signature", true, true);
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, inPath,null, "/render/signature", true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/hash/{hash}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexByHash(@PathParam("hash") String hash58, @QueryParam("secret") String secret58) {
|
||||
public HttpServletResponse getIndexByHash(@PathParam("hash") String hash58, @QueryParam("secret") String secret58,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.WEBSITE, "/", secret58, "/render/hash", true, false);
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.WEBSITE, "/", secret58, "/render/hash", true, false, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/hash/{hash}/{path:.*}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getPathByHash(@PathParam("hash") String hash58, @PathParam("path") String inPath,
|
||||
@QueryParam("secret") String secret58) {
|
||||
@QueryParam("secret") String secret58,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.WEBSITE, inPath, secret58, "/render/hash", true, false);
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.WEBSITE, inPath, secret58, "/render/hash", true, false, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -171,29 +177,35 @@ public class RenderResource {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getPathByName(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("path") String inPath) {
|
||||
@PathParam("path") String inPath,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, name, service, null);
|
||||
String prefix = String.format("/render/%s", service);
|
||||
return this.get(name, ResourceIdType.NAME, service, inPath, null, prefix, true, true);
|
||||
return this.get(name, ResourceIdType.NAME, service, inPath, null, prefix, true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{service}/{name}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexByName(@PathParam("service") Service service,
|
||||
@PathParam("name") String name) {
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, name, service, null);
|
||||
String prefix = String.format("/render/%s", service);
|
||||
return this.get(name, ResourceIdType.NAME, service, "/", null, prefix, true, true);
|
||||
return this.get(name, ResourceIdType.NAME, service, "/", null, prefix, true, true, theme);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String inPath,
|
||||
String secret58, String prefix, boolean usePrefix, boolean async) {
|
||||
String secret58, String prefix, boolean usePrefix, boolean async, String theme) {
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, inPath,
|
||||
secret58, prefix, usePrefix, async, request, response, context);
|
||||
|
||||
if (theme != null) {
|
||||
renderer.setTheme(theme);
|
||||
}
|
||||
return renderer.render();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
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.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -44,6 +46,7 @@ import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@Path("/transactions")
|
||||
@Tag(name = "Transactions")
|
||||
@@ -363,6 +366,83 @@ public class TransactionsResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/unitfee")
|
||||
@Operation(
|
||||
summary = "Get transaction unit fee",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public long getTransactionUnitFee(@QueryParam("txType") TransactionType txType,
|
||||
@QueryParam("timestamp") Long timestamp,
|
||||
@QueryParam("level") Integer accountLevel) {
|
||||
try {
|
||||
if (timestamp == null) {
|
||||
timestamp = NTP.getTime();
|
||||
}
|
||||
|
||||
Constructor<?> constructor = txType.constructor;
|
||||
Transaction transaction = (Transaction) constructor.newInstance(null, null);
|
||||
// FUTURE: add accountLevel parameter to transaction.getUnitFee() if needed
|
||||
return transaction.getUnitFee(timestamp);
|
||||
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/fee")
|
||||
@Operation(
|
||||
summary = "Get recommended fee for supplied transaction data",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@ApiErrors({
|
||||
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public long getRecommendedTransactionFee(String rawInputBytes58) {
|
||||
byte[] rawInputBytes = Base58.decode(rawInputBytes58);
|
||||
if (rawInputBytes.length == 0)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.JSON);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Append null signature on the end before transformation
|
||||
byte[] rawBytes = Bytes.concat(rawInputBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
|
||||
|
||||
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
|
||||
if (transactionData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
return transaction.calcRecommendedFee();
|
||||
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/creator/{publickey}")
|
||||
@Operation(
|
||||
@@ -558,7 +638,10 @@ public class TransactionsResource {
|
||||
ApiError.BLOCKCHAIN_NEEDS_SYNC, ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public String processTransaction(String rawBytes58) {
|
||||
if (!Controller.getInstance().isUpToDate())
|
||||
// Only allow a transaction to be processed if our latest block is less than 30 minutes old
|
||||
// If older than this, we should first wait until the blockchain is synced
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
|
||||
|
||||
byte[] rawBytes = Base58.decode(rawBytes58);
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
|
||||
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.PresenceTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -99,13 +100,13 @@ public class PresenceWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
// We use NewBlockEvent as a proxy for 1-minute timer
|
||||
if (!(event instanceof Controller.NewTransactionEvent) && !(event instanceof Controller.NewBlockEvent))
|
||||
// We use Synchronizer.NewChainTipEvent as a proxy for 1-minute timer
|
||||
if (!(event instanceof Controller.NewTransactionEvent) && !(event instanceof Synchronizer.NewChainTipEvent))
|
||||
return;
|
||||
|
||||
removeOldEntries();
|
||||
|
||||
if (event instanceof Controller.NewBlockEvent)
|
||||
if (event instanceof Synchronizer.NewChainTipEvent)
|
||||
// We only wanted a chance to cull old entries
|
||||
return;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.eclipse.jetty.websocket.api.annotations.WebSocket;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.api.model.CrossChainOfferSummary;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
@@ -80,10 +81,10 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof Controller.NewBlockEvent))
|
||||
if (!(event instanceof Synchronizer.NewChainTipEvent))
|
||||
return;
|
||||
|
||||
BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
|
||||
BlockData blockData = ((Synchronizer.NewChainTipEvent) event).getNewChainTip();
|
||||
|
||||
// Process any new info
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.qortal.api.websocket;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.annotations.*;
|
||||
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.data.network.TradePresenceData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.*;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class TradePresenceWebSocket extends ApiWebSocket implements Listener {
|
||||
|
||||
/** Map key is public key in base58, map value is trade presence */
|
||||
private static final Map<String, TradePresenceData> currentEntries = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
@Override
|
||||
public void configure(WebSocketServletFactory factory) {
|
||||
factory.register(TradePresenceWebSocket.class);
|
||||
|
||||
populateCurrentInfo();
|
||||
|
||||
EventBus.INSTANCE.addListener(this::listen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
// XXX - Suggest we change this to something like Synchronizer.NewChainTipEvent?
|
||||
// We use NewBlockEvent as a proxy for 1-minute timer
|
||||
if (!(event instanceof TradeBot.TradePresenceEvent) && !(event instanceof Controller.NewBlockEvent))
|
||||
return;
|
||||
|
||||
removeOldEntries();
|
||||
|
||||
if (event instanceof Controller.NewBlockEvent)
|
||||
// We only wanted a chance to cull old entries
|
||||
return;
|
||||
|
||||
TradePresenceData tradePresence = ((TradeBot.TradePresenceEvent) event).getTradePresenceData();
|
||||
|
||||
boolean somethingChanged = mergePresence(tradePresence);
|
||||
|
||||
if (!somethingChanged)
|
||||
// nothing changed
|
||||
return;
|
||||
|
||||
List<TradePresenceData> tradePresences = Collections.singletonList(tradePresence);
|
||||
|
||||
// Notify sessions
|
||||
for (Session session : getSessions()) {
|
||||
sendTradePresences(session, tradePresences);
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketConnect
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
|
||||
List<TradePresenceData> tradePresences;
|
||||
|
||||
synchronized (currentEntries) {
|
||||
tradePresences = List.copyOf(currentEntries.values());
|
||||
}
|
||||
|
||||
if (!sendTradePresences(session, tradePresences)) {
|
||||
session.close(4002, "websocket issue");
|
||||
return;
|
||||
}
|
||||
|
||||
super.onWebSocketConnect(session);
|
||||
}
|
||||
|
||||
@OnWebSocketClose
|
||||
@Override
|
||||
public void onWebSocketClose(Session session, int statusCode, String reason) {
|
||||
// clean up
|
||||
super.onWebSocketClose(session, statusCode, reason);
|
||||
}
|
||||
|
||||
@OnWebSocketError
|
||||
public void onWebSocketError(Session session, Throwable throwable) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
private boolean sendTradePresences(Session session, List<TradePresenceData> tradePresences) {
|
||||
try {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
marshall(stringWriter, tradePresences);
|
||||
|
||||
String output = stringWriter.toString();
|
||||
session.getRemote().sendStringByFuture(output);
|
||||
} catch (IOException e) {
|
||||
// No output this time?
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void populateCurrentInfo() {
|
||||
// We want ALL trade presences
|
||||
TradeBot.getInstance().getAllTradePresences().stream()
|
||||
.forEach(TradePresenceWebSocket::mergePresence);
|
||||
}
|
||||
|
||||
/** Merge trade presence into cache of current entries, returns true if cache was updated. */
|
||||
private static boolean mergePresence(TradePresenceData tradePresence) {
|
||||
// Put/replace for this publickey making sure we keep newest timestamp
|
||||
String pubKey58 = Base58.encode(tradePresence.getPublicKey());
|
||||
|
||||
TradePresenceData newEntry = currentEntries.compute(pubKey58, (k, v) -> v == null || v.getTimestamp() < tradePresence.getTimestamp() ? tradePresence : v);
|
||||
|
||||
return newEntry == tradePresence;
|
||||
}
|
||||
|
||||
private static void removeOldEntries() {
|
||||
long now = NTP.getTime();
|
||||
|
||||
currentEntries.values().removeIf(v -> v.getTimestamp() < now);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,8 +13,11 @@ public class ArbitraryDataBuildQueueItem extends ArbitraryDataResource {
|
||||
private final Long creationTimestamp;
|
||||
private Long buildStartTimestamp = null;
|
||||
private Long buildEndTimestamp = null;
|
||||
private Integer priority = 0;
|
||||
private boolean failed = false;
|
||||
|
||||
private static int HIGH_PRIORITY_THRESHOLD = 5;
|
||||
|
||||
/* The maximum amount of time to spend on a single build */
|
||||
// TODO: interrupt an in-progress build
|
||||
public static long BUILD_TIMEOUT = 60*1000L; // 60 seconds
|
||||
@@ -27,13 +30,20 @@ public class ArbitraryDataBuildQueueItem extends ArbitraryDataResource {
|
||||
this.creationTimestamp = NTP.getTime();
|
||||
}
|
||||
|
||||
public void prepareForBuild() {
|
||||
this.buildStartTimestamp = NTP.getTime();
|
||||
}
|
||||
|
||||
public void build() throws IOException, DataException, MissingDataException {
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
this.buildStartTimestamp = null;
|
||||
throw new DataException("NTP time hasn't synced yet");
|
||||
}
|
||||
|
||||
this.buildStartTimestamp = now;
|
||||
if (this.buildStartTimestamp == null) {
|
||||
this.buildStartTimestamp = now;
|
||||
}
|
||||
ArbitraryDataReader arbitraryDataReader =
|
||||
new ArbitraryDataReader(this.resourceId, this.resourceIdType, this.service, this.identifier);
|
||||
|
||||
@@ -70,6 +80,21 @@ public class ArbitraryDataBuildQueueItem extends ArbitraryDataResource {
|
||||
return this.buildStartTimestamp;
|
||||
}
|
||||
|
||||
public Integer getPriority() {
|
||||
if (this.priority != null) {
|
||||
return this.priority;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void setPriority(Integer priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public boolean isHighPriority() {
|
||||
return this.priority >= HIGH_PRIORITY_THRESHOLD;
|
||||
}
|
||||
|
||||
public void setFailed(boolean failed) {
|
||||
this.failed = failed;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ public class ArbitraryDataCache {
|
||||
}
|
||||
|
||||
// No need to invalidate the cache
|
||||
// Remember that it's up to date, so that we won't check again for a while
|
||||
ArbitraryDataManager.getInstance().addResourceToCache(this.getArbitraryDataResource());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -84,14 +87,7 @@ public class ArbitraryDataCache {
|
||||
|
||||
// If the state's sig doesn't match the latest transaction's sig, we need to invalidate
|
||||
// This means that an updated layer is available
|
||||
if (this.shouldInvalidateDueToSignatureMismatch()) {
|
||||
|
||||
// Add to the in-memory cache first, so that we won't check again for a while
|
||||
ArbitraryDataManager.getInstance().addResourceToCache(this.getArbitraryDataResource());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return this.shouldInvalidateDueToSignatureMismatch();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,7 +53,8 @@ public class ArbitraryDataFile {
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFile.class);
|
||||
|
||||
public static final long MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MiB
|
||||
public static final int CHUNK_SIZE = 1 * 1024 * 1024; // 1MiB
|
||||
protected static final int MAX_CHUNK_SIZE = 1 * 1024 * 1024; // 1MiB
|
||||
public static final int CHUNK_SIZE = 512 * 1024; // 0.5MiB
|
||||
public static int SHORT_DIGEST_LENGTH = 8;
|
||||
|
||||
protected Path filePath;
|
||||
@@ -72,7 +73,6 @@ public class ArbitraryDataFile {
|
||||
}
|
||||
|
||||
public ArbitraryDataFile(String hash58, byte[] signature) throws DataException {
|
||||
this.createDataDirectory();
|
||||
this.filePath = ArbitraryDataFile.getOutputFilePath(hash58, signature, false);
|
||||
this.chunks = new ArrayList<>();
|
||||
this.hash58 = hash58;
|
||||
@@ -93,10 +93,12 @@ public class ArbitraryDataFile {
|
||||
File outputFile = outputFilePath.toFile();
|
||||
try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
|
||||
outputStream.write(fileContent);
|
||||
outputStream.close();
|
||||
this.filePath = outputFilePath;
|
||||
// Verify hash
|
||||
if (!this.hash58.equals(this.digest58())) {
|
||||
LOGGER.error("Hash {} does not match file digest {}", this.hash58, this.digest58());
|
||||
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");
|
||||
}
|
||||
@@ -110,6 +112,9 @@ public class ArbitraryDataFile {
|
||||
}
|
||||
|
||||
public static ArbitraryDataFile fromHash(byte[] hash, byte[] signature) throws DataException {
|
||||
if (hash == null) {
|
||||
return null;
|
||||
}
|
||||
return ArbitraryDataFile.fromHash58(Base58.encode(hash), signature);
|
||||
}
|
||||
|
||||
@@ -146,19 +151,6 @@ public class ArbitraryDataFile {
|
||||
return ArbitraryDataFile.fromPath(Paths.get(file.getPath()), signature);
|
||||
}
|
||||
|
||||
private boolean createDataDirectory() {
|
||||
// Create the data directory if it doesn't exist
|
||||
String dataPath = Settings.getInstance().getDataPath();
|
||||
Path dataDirectory = Paths.get(dataPath);
|
||||
try {
|
||||
Files.createDirectories(dataDirectory);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Unable to create data directory");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Path copyToDataDirectory(Path sourcePath, byte[] signature) throws DataException {
|
||||
if (this.hash58 == null || this.filePath == null) {
|
||||
return null;
|
||||
@@ -366,6 +358,21 @@ public class ArbitraryDataFile {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean delete(int attempts) {
|
||||
// Keep trying to delete the data until it is deleted, or we reach 10 attempts
|
||||
for (int i=0; i<attempts; i++) {
|
||||
if (this.delete()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1000L);
|
||||
} catch (InterruptedException e) {
|
||||
// Fall through to exit method
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean deleteAllChunks() {
|
||||
boolean success = false;
|
||||
|
||||
@@ -439,6 +446,11 @@ public class ArbitraryDataFile {
|
||||
return chunk.exists();
|
||||
}
|
||||
}
|
||||
if (Arrays.equals(hash, this.metadataHash)) {
|
||||
if (this.metadataFile != null) {
|
||||
return this.metadataFile.exists();
|
||||
}
|
||||
}
|
||||
if (Arrays.equals(this.getHash(), hash)) {
|
||||
return this.exists();
|
||||
}
|
||||
@@ -455,9 +467,6 @@ public class ArbitraryDataFile {
|
||||
|
||||
if (this.metadataFile == null) {
|
||||
this.metadataFile = ArbitraryDataFile.fromHash(this.metadataHash, this.signature);
|
||||
if (!metadataFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the metadata file doesn't exist, we can't check if we have the chunks
|
||||
@@ -471,6 +480,14 @@ public class ArbitraryDataFile {
|
||||
|
||||
// Read the metadata
|
||||
List<byte[]> chunks = metadata.getChunks();
|
||||
|
||||
// If the chunks array is empty, then this resource has no chunks,
|
||||
// so we must return false to avoid confusing the caller.
|
||||
if (chunks.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, we need to check each chunk individually
|
||||
for (byte[] chunkHash : chunks) {
|
||||
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash(chunkHash, this.signature);
|
||||
if (!chunk.exists()) {
|
||||
@@ -496,9 +513,6 @@ public class ArbitraryDataFile {
|
||||
|
||||
if (this.metadataFile == null) {
|
||||
this.metadataFile = ArbitraryDataFile.fromHash(this.metadataHash, this.signature);
|
||||
if (!metadataFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the metadata file doesn't exist, we can't check if we have any chunks
|
||||
@@ -540,6 +554,50 @@ public class ArbitraryDataFile {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of file hashes for this transaction that we do not hold locally
|
||||
*
|
||||
* @return a List of chunk hashes, or null if we are unable to determine what is missing
|
||||
*/
|
||||
public List<byte[]> missingHashes() {
|
||||
List<byte[]> missingHashes = new ArrayList<>();
|
||||
try {
|
||||
if (this.metadataHash == null) {
|
||||
// We don't have any metadata so can't check if we have the chunks
|
||||
// Even if this transaction has no chunks, we don't have the file either (already checked above)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.metadataFile == null) {
|
||||
this.metadataFile = ArbitraryDataFile.fromHash(this.metadataHash, this.signature);
|
||||
}
|
||||
|
||||
// If the metadata file doesn't exist, we can't check if we have the chunks
|
||||
if (!metadataFile.getFilePath().toFile().exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.metadata == null) {
|
||||
this.setMetadata(new ArbitraryDataTransactionMetadata(this.metadataFile.getFilePath()));
|
||||
}
|
||||
|
||||
// Read the metadata
|
||||
List<byte[]> chunks = metadata.getChunks();
|
||||
for (byte[] chunkHash : chunks) {
|
||||
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash(chunkHash, this.signature);
|
||||
if (!chunk.exists()) {
|
||||
missingHashes.add(chunkHash);
|
||||
}
|
||||
}
|
||||
|
||||
return missingHashes;
|
||||
|
||||
} catch (DataException e) {
|
||||
// Something went wrong, so we can't make a sensible decision
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsChunk(byte[] hash) {
|
||||
for (ArbitraryDataFileChunk chunk : this.chunks) {
|
||||
if (Arrays.equals(hash, chunk.getHash())) {
|
||||
@@ -728,6 +786,10 @@ public class ArbitraryDataFile {
|
||||
this.loadMetadata();
|
||||
}
|
||||
|
||||
public ArbitraryDataTransactionMetadata getMetadata() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.shortHash58();
|
||||
|
||||
@@ -40,8 +40,8 @@ public class ArbitraryDataFileChunk extends ArbitraryDataFile {
|
||||
try {
|
||||
// Validate the file size (chunks have stricter limits)
|
||||
long fileSize = Files.size(this.filePath);
|
||||
if (fileSize > CHUNK_SIZE) {
|
||||
LOGGER.error(String.format("DataFileChunk is too large: %d bytes (max chunk size: %d bytes)", fileSize, CHUNK_SIZE));
|
||||
if (fileSize > MAX_CHUNK_SIZE) {
|
||||
LOGGER.error(String.format("DataFileChunk is too large: %d bytes (max chunk size: %d bytes)", fileSize, MAX_CHUNK_SIZE));
|
||||
return ValidationResult.FILE_TOO_LARGE;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,10 +122,22 @@ public class ArbitraryDataReader {
|
||||
* This adds the build task to a queue, and the result will be cached when complete
|
||||
* To check the status of the build, periodically call isCachedDataAvailable()
|
||||
* Once it returns true, you can then use getFilePath() to access the data itself.
|
||||
*
|
||||
* @param overwrite - set to true to force rebuild an existing cache
|
||||
* @return true if added or already present in queue; false if not
|
||||
*/
|
||||
public boolean loadAsynchronously() {
|
||||
return ArbitraryDataBuildManager.getInstance().addToBuildQueue(this.createQueueItem());
|
||||
public boolean loadAsynchronously(boolean overwrite, int priority) {
|
||||
ArbitraryDataCache cache = new ArbitraryDataCache(this.uncompressedPath, overwrite,
|
||||
this.resourceId, this.resourceIdType, this.service, this.identifier);
|
||||
if (cache.isCachedDataAvailable()) {
|
||||
// Use cached data
|
||||
this.filePath = this.uncompressedPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
ArbitraryDataBuildQueueItem item = this.createQueueItem();
|
||||
item.setPriority(priority);
|
||||
return ArbitraryDataBuildManager.getInstance().addToBuildQueue(item);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,7 +375,7 @@ public class ArbitraryDataReader {
|
||||
}
|
||||
|
||||
// Throw a missing data exception, which allows subsequent layers to fetch data
|
||||
LOGGER.debug(message);
|
||||
LOGGER.trace(message);
|
||||
throw new MissingDataException(message);
|
||||
}
|
||||
}
|
||||
@@ -458,12 +470,18 @@ public class ArbitraryDataReader {
|
||||
throw new DataException(String.format("Unable to unzip file: %s", e.getMessage()));
|
||||
}
|
||||
|
||||
// Replace filePath pointer with the uncompressed file path
|
||||
if (!this.uncompressedPath.toFile().exists()) {
|
||||
throw new DataException(String.format("Unable to unzip file: %s", this.filePath));
|
||||
}
|
||||
|
||||
// Delete original compressed file
|
||||
if (FilesystemUtils.pathInsideDataOrTempPath(this.filePath)) {
|
||||
if (Files.exists(this.filePath)) {
|
||||
Files.delete(this.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace filePath pointer with the uncompressed file path
|
||||
this.filePath = this.uncompressedPath;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ public class ArbitraryDataRenderer {
|
||||
private final String resourceId;
|
||||
private final ResourceIdType resourceIdType;
|
||||
private final Service service;
|
||||
private String theme = "light";
|
||||
private String inPath;
|
||||
private final String secret58;
|
||||
private final String prefix;
|
||||
@@ -76,8 +77,8 @@ public class ArbitraryDataRenderer {
|
||||
if (!arbitraryDataReader.isCachedDataAvailable()) {
|
||||
// If async is requested, show a loading screen whilst build is in progress
|
||||
if (async) {
|
||||
arbitraryDataReader.loadAsynchronously();
|
||||
return this.getLoadingResponse(service, resourceId);
|
||||
arbitraryDataReader.loadAsynchronously(false, 10);
|
||||
return this.getLoadingResponse(service, resourceId, theme);
|
||||
}
|
||||
|
||||
// Otherwise, loop until we have data
|
||||
@@ -119,7 +120,7 @@ public class ArbitraryDataRenderer {
|
||||
byte[] data = Files.readAllBytes(Paths.get(filePath)); // TODO: limit file size that can be read into memory
|
||||
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data);
|
||||
htmlParser.addAdditionalHeaderTags();
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline'; media-src 'self' blob:");
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' blob:");
|
||||
response.setContentType(context.getMimeType(filename));
|
||||
response.setContentLength(htmlParser.getData().length);
|
||||
response.getOutputStream().write(htmlParser.getData());
|
||||
@@ -128,7 +129,7 @@ public class ArbitraryDataRenderer {
|
||||
// Regular file - can be streamed directly
|
||||
File file = new File(filePath);
|
||||
FileInputStream inputStream = new FileInputStream(file);
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline'; media-src 'self' blob:");
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self'");
|
||||
response.setContentType(context.getMimeType(filename));
|
||||
int bytesRead, length = 0;
|
||||
byte[] buffer = new byte[10240];
|
||||
@@ -171,7 +172,7 @@ public class ArbitraryDataRenderer {
|
||||
return userPath;
|
||||
}
|
||||
|
||||
private HttpServletResponse getLoadingResponse(Service service, String name) {
|
||||
private HttpServletResponse getLoadingResponse(Service service, String name, String theme) {
|
||||
String responseString = "";
|
||||
URL url = Resources.getResource("loading/index.html");
|
||||
try {
|
||||
@@ -180,6 +181,7 @@ public class ArbitraryDataRenderer {
|
||||
// Replace vars
|
||||
responseString = responseString.replace("%%SERVICE%%", service.toString());
|
||||
responseString = responseString.replace("%%NAME%%", name);
|
||||
responseString = responseString.replace("%%THEME%%", theme);
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to show loading screen: {}", e.getMessage());
|
||||
@@ -210,4 +212,8 @@ public class ArbitraryDataRenderer {
|
||||
return indexFiles;
|
||||
}
|
||||
|
||||
public void setTheme(String theme) {
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.qortal.arbitrary;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
@@ -37,7 +38,10 @@ public class ArbitraryDataResource {
|
||||
|
||||
private List<ArbitraryTransactionData> transactions;
|
||||
private ArbitraryTransactionData latestPutTransaction;
|
||||
private ArbitraryTransactionData latestTransaction;
|
||||
private int layerCount;
|
||||
private Integer localChunkCount = null;
|
||||
private Integer totalChunkCount = null;
|
||||
|
||||
public ArbitraryDataResource(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
|
||||
this.resourceId = resourceId.toLowerCase();
|
||||
@@ -51,50 +55,83 @@ public class ArbitraryDataResource {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
public ArbitraryResourceStatus getStatus() {
|
||||
public ArbitraryResourceStatus getStatus(boolean quick) {
|
||||
// Calculate the chunk counts
|
||||
// Avoid this for "quick" statuses, to speed things up
|
||||
if (!quick) {
|
||||
this.calculateChunkCounts();
|
||||
}
|
||||
|
||||
if (resourceIdType != ResourceIdType.NAME) {
|
||||
// We only support statuses for resources with a name
|
||||
return new ArbitraryResourceStatus(Status.UNSUPPORTED);
|
||||
return new ArbitraryResourceStatus(Status.UNSUPPORTED, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
|
||||
// Check if the name is blocked
|
||||
if (ResourceListManager.getInstance()
|
||||
.listContains("blockedNames", this.resourceId, false)) {
|
||||
return new ArbitraryResourceStatus(Status.BLOCKED);
|
||||
return new ArbitraryResourceStatus(Status.BLOCKED, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
|
||||
// Check if a build has failed
|
||||
ArbitraryDataBuildQueueItem queueItem =
|
||||
new ArbitraryDataBuildQueueItem(resourceId, resourceIdType, service, identifier);
|
||||
if (ArbitraryDataBuildManager.getInstance().isInFailedBuildsList(queueItem)) {
|
||||
return new ArbitraryResourceStatus(Status.BUILD_FAILED, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
|
||||
// Firstly check the cache to see if it's already built
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(
|
||||
resourceId, resourceIdType, service, identifier);
|
||||
if (arbitraryDataReader.isCachedDataAvailable()) {
|
||||
return new ArbitraryResourceStatus(Status.READY);
|
||||
}
|
||||
|
||||
// Next check if there's a build in progress
|
||||
ArbitraryDataBuildQueueItem queueItem =
|
||||
new ArbitraryDataBuildQueueItem(resourceId, resourceIdType, service, identifier);
|
||||
if (ArbitraryDataBuildManager.getInstance().isInBuildQueue(queueItem)) {
|
||||
return new ArbitraryResourceStatus(Status.BUILDING);
|
||||
}
|
||||
|
||||
// Check if a build has failed
|
||||
if (ArbitraryDataBuildManager.getInstance().isInFailedBuildsList(queueItem)) {
|
||||
return new ArbitraryResourceStatus(Status.BUILD_FAILED);
|
||||
return new ArbitraryResourceStatus(Status.READY, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
|
||||
// Check if we have all data locally for this resource
|
||||
if (!this.allFilesDownloaded()) {
|
||||
if (this.isDownloading()) {
|
||||
return new ArbitraryResourceStatus(Status.DOWNLOADING);
|
||||
return new ArbitraryResourceStatus(Status.DOWNLOADING, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
else if (this.isDataPotentiallyAvailable()) {
|
||||
return new ArbitraryResourceStatus(Status.PUBLISHED);
|
||||
return new ArbitraryResourceStatus(Status.PUBLISHED, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
return new ArbitraryResourceStatus(Status.MISSING_DATA);
|
||||
return new ArbitraryResourceStatus(Status.MISSING_DATA, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
|
||||
// Check if there's a build in progress
|
||||
if (ArbitraryDataBuildManager.getInstance().isInBuildQueue(queueItem)) {
|
||||
return new ArbitraryResourceStatus(Status.BUILDING, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
|
||||
// We have all data locally
|
||||
return new ArbitraryResourceStatus(Status.DOWNLOADED);
|
||||
return new ArbitraryResourceStatus(Status.DOWNLOADED, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
|
||||
public ArbitraryDataTransactionMetadata getLatestTransactionMetadata() {
|
||||
this.fetchLatestTransaction();
|
||||
|
||||
if (latestTransaction != null) {
|
||||
byte[] signature = latestTransaction.getSignature();
|
||||
byte[] metadataHash = latestTransaction.getMetadataHash();
|
||||
if (metadataHash == null) {
|
||||
// This resource doesn't have metadata
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
|
||||
if (metadataFile.exists()) {
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = new ArbitraryDataTransactionMetadata(metadataFile.getFilePath());
|
||||
transactionMetadata.read();
|
||||
return transactionMetadata;
|
||||
}
|
||||
|
||||
} catch (DataException | IOException e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean delete() {
|
||||
@@ -128,6 +165,13 @@ public class ArbitraryDataResource {
|
||||
}
|
||||
|
||||
public void deleteCache() throws IOException {
|
||||
// Don't delete anything if there's a build in progress
|
||||
ArbitraryDataBuildQueueItem queueItem =
|
||||
new ArbitraryDataBuildQueueItem(resourceId, resourceIdType, service, identifier);
|
||||
if (ArbitraryDataBuildManager.getInstance().isInBuildQueue(queueItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String baseDir = Settings.getInstance().getTempDataPath();
|
||||
String identifier = this.identifier != null ? this.identifier : "default";
|
||||
Path cachePath = Paths.get(baseDir, "reader", this.resourceIdType.toString(), this.resourceId, this.service.toString(), identifier);
|
||||
@@ -140,6 +184,12 @@ public class ArbitraryDataResource {
|
||||
}
|
||||
|
||||
private boolean allFilesDownloaded() {
|
||||
// Use chunk counts to speed things up if we can
|
||||
if (this.localChunkCount != null && this.totalChunkCount != null &&
|
||||
this.localChunkCount >= this.totalChunkCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
|
||||
@@ -158,6 +208,25 @@ public class ArbitraryDataResource {
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateChunkCounts() {
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList = new ArrayList<>(this.transactions);
|
||||
int localChunkCount = 0;
|
||||
int totalChunkCount = 0;
|
||||
|
||||
for (ArbitraryTransactionData transactionData : transactionDataList) {
|
||||
localChunkCount += ArbitraryTransactionUtils.ourChunkCount(transactionData);
|
||||
totalChunkCount += ArbitraryTransactionUtils.totalChunkCount(transactionData);
|
||||
}
|
||||
|
||||
this.localChunkCount = localChunkCount;
|
||||
this.totalChunkCount = totalChunkCount;
|
||||
|
||||
} catch (DataException e) {}
|
||||
}
|
||||
|
||||
private boolean isRateLimited() {
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
@@ -266,6 +335,32 @@ public class ArbitraryDataResource {
|
||||
|
||||
this.transactions = transactionDataList;
|
||||
this.layerCount = transactionDataList.size();
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info(String.format("Repository error when fetching transactions for resource %s: %s", this, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchLatestTransaction() {
|
||||
if (this.latestTransaction != null) {
|
||||
// Already fetched
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Get the most recent transaction
|
||||
ArbitraryTransactionData latestTransaction = repository.getArbitraryRepository()
|
||||
.getLatestTransaction(this.resourceId, this.service, null, this.identifier);
|
||||
if (latestTransaction == null) {
|
||||
String message = String.format("Couldn't find transaction for name %s, service %s and identifier %s",
|
||||
this.resourceId, this.service, this.identifierString());
|
||||
throw new DataException(message);
|
||||
}
|
||||
this.latestTransaction = latestTransaction;
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info(String.format("Repository error when fetching latest transaction for resource %s: %s", this, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
import org.qortal.arbitrary.ArbitraryDataDiff.*;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataPatch;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
@@ -27,6 +28,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
public class ArbitraryDataTransactionBuilder {
|
||||
@@ -51,13 +53,20 @@ public class ArbitraryDataTransactionBuilder {
|
||||
private final String identifier;
|
||||
private final Repository repository;
|
||||
|
||||
// Metadata
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final List<String> tags;
|
||||
private final Category category;
|
||||
|
||||
private int chunkSize = ArbitraryDataFile.CHUNK_SIZE;
|
||||
|
||||
private ArbitraryTransactionData arbitraryTransactionData;
|
||||
private ArbitraryDataFile arbitraryDataFile;
|
||||
|
||||
public ArbitraryDataTransactionBuilder(Repository repository, String publicKey58, Path path, String name,
|
||||
Method method, Service service, String identifier) {
|
||||
Method method, Service service, String identifier,
|
||||
String title, String description, List<String> tags, Category category) {
|
||||
this.repository = repository;
|
||||
this.publicKey58 = publicKey58;
|
||||
this.path = path;
|
||||
@@ -70,6 +79,12 @@ public class ArbitraryDataTransactionBuilder {
|
||||
identifier = null;
|
||||
}
|
||||
this.identifier = identifier;
|
||||
|
||||
// Metadata (optional)
|
||||
this.title = ArbitraryDataTransactionMetadata.limitTitle(title);
|
||||
this.description = ArbitraryDataTransactionMetadata.limitDescription(description);
|
||||
this.tags = ArbitraryDataTransactionMetadata.limitTags(tags);
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public void build() throws DataException {
|
||||
@@ -108,6 +123,10 @@ public class ArbitraryDataTransactionBuilder {
|
||||
return Method.PUT;
|
||||
}
|
||||
|
||||
// Get existing metadata and see if it matches the new metadata
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(this.name, ResourceIdType.NAME, this.service, this.identifier);
|
||||
ArbitraryDataTransactionMetadata existingMetadata = resource.getLatestTransactionMetadata();
|
||||
|
||||
try {
|
||||
// Check layer count
|
||||
int layerCount = reader.getLayerCount();
|
||||
@@ -118,7 +137,23 @@ public class ArbitraryDataTransactionBuilder {
|
||||
|
||||
// Check size of differences between this layer and previous layer
|
||||
ArbitraryDataCreatePatch patch = new ArbitraryDataCreatePatch(reader.getFilePath(), this.path, reader.getLatestSignature());
|
||||
patch.create();
|
||||
try {
|
||||
patch.create();
|
||||
}
|
||||
catch (DataException | IOException e) {
|
||||
// Handle matching states separately, as it's best to block transactions with duplicate states
|
||||
if (e.getMessage().equals("Current state matches previous state. Nothing to do.")) {
|
||||
// Only throw an exception if the metadata is also identical, as well as the data
|
||||
if (this.isMetadataEqual(existingMetadata)) {
|
||||
throw new DataException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("Caught exception when creating patch: {}", e.getMessage());
|
||||
LOGGER.info("Unable to load existing resource - using PUT to overwrite it.");
|
||||
return Method.PUT;
|
||||
}
|
||||
|
||||
long diffSize = FilesystemUtils.getDirectorySize(patch.getFinalPath());
|
||||
long existingStateSize = FilesystemUtils.getDirectorySize(reader.getFilePath());
|
||||
double difference = (double) diffSize / (double) existingStateSize;
|
||||
@@ -155,11 +190,8 @@ public class ArbitraryDataTransactionBuilder {
|
||||
// State is appropriate for a PATCH transaction
|
||||
return Method.PATCH;
|
||||
}
|
||||
catch (IOException | DataException e) {
|
||||
// Handle matching states separately, as it's best to block transactions with duplicate states
|
||||
if (e.getMessage().equals("Current state matches previous state. Nothing to do.")) {
|
||||
throw new DataException(e.getMessage());
|
||||
}
|
||||
catch (IOException e) {
|
||||
// IMPORTANT: Don't catch DataException here, as they must be passed to the caller
|
||||
LOGGER.info("Caught exception: {}", e.getMessage());
|
||||
LOGGER.info("Unable to load existing resource - using PUT to overwrite it.");
|
||||
return Method.PUT;
|
||||
@@ -200,7 +232,8 @@ public class ArbitraryDataTransactionBuilder {
|
||||
// FUTURE? Use zip compression for directories, or no compression for single files
|
||||
// Compression compression = (path.toFile().isDirectory()) ? Compression.ZIP : Compression.NONE;
|
||||
|
||||
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(path, name, service, identifier, method, compression);
|
||||
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(path, name, service, identifier, method,
|
||||
compression, title, description, tags, category);
|
||||
try {
|
||||
arbitraryDataWriter.setChunkSize(this.chunkSize);
|
||||
arbitraryDataWriter.save();
|
||||
@@ -253,6 +286,22 @@ public class ArbitraryDataTransactionBuilder {
|
||||
|
||||
}
|
||||
|
||||
private boolean isMetadataEqual(ArbitraryDataTransactionMetadata existingMetadata) {
|
||||
if (!Objects.equals(existingMetadata.getTitle(), this.title)) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(existingMetadata.getDescription(), this.description)) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(existingMetadata.getCategory(), this.category)) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(existingMetadata.getTags(), this.tags)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void computeNonce() throws DataException {
|
||||
if (this.arbitraryTransactionData == null) {
|
||||
throw new DataException("Arbitrary transaction data is required to compute nonce");
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData.*;
|
||||
@@ -28,6 +29,10 @@ import java.nio.file.Paths;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ArbitraryDataWriter {
|
||||
|
||||
@@ -40,6 +45,12 @@ public class ArbitraryDataWriter {
|
||||
private final Method method;
|
||||
private final Compression compression;
|
||||
|
||||
// Metadata
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final List<String> tags;
|
||||
private final Category category;
|
||||
|
||||
private int chunkSize = ArbitraryDataFile.CHUNK_SIZE;
|
||||
|
||||
private SecretKey aesKey;
|
||||
@@ -50,7 +61,8 @@ public class ArbitraryDataWriter {
|
||||
private Path compressedPath;
|
||||
private Path encryptedPath;
|
||||
|
||||
public ArbitraryDataWriter(Path filePath, String name, Service service, String identifier, Method method, Compression compression) {
|
||||
public ArbitraryDataWriter(Path filePath, String name, Service service, String identifier, Method method, Compression compression,
|
||||
String title, String description, List<String> tags, Category category) {
|
||||
this.filePath = filePath;
|
||||
this.name = name;
|
||||
this.service = service;
|
||||
@@ -62,6 +74,12 @@ public class ArbitraryDataWriter {
|
||||
identifier = null;
|
||||
}
|
||||
this.identifier = identifier;
|
||||
|
||||
// Metadata (optional)
|
||||
this.title = ArbitraryDataTransactionMetadata.limitTitle(title);
|
||||
this.description = ArbitraryDataTransactionMetadata.limitDescription(description);
|
||||
this.tags = ArbitraryDataTransactionMetadata.limitTags(tags);
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public void save() throws IOException, DataException, InterruptedException, MissingDataException {
|
||||
@@ -258,12 +276,16 @@ public class ArbitraryDataWriter {
|
||||
|
||||
private void createMetadataFile() throws IOException, DataException {
|
||||
// If we have at least one chunk, we need to create an index file containing their hashes
|
||||
if (this.arbitraryDataFile.chunkCount() > 1) {
|
||||
if (this.needsMetadataFile()) {
|
||||
// Create the JSON file
|
||||
Path chunkFilePath = Paths.get(this.workingPath.toString(), "metadata.json");
|
||||
ArbitraryDataTransactionMetadata chunkMetadata = new ArbitraryDataTransactionMetadata(chunkFilePath);
|
||||
chunkMetadata.setChunks(this.arbitraryDataFile.chunkHashList());
|
||||
chunkMetadata.write();
|
||||
ArbitraryDataTransactionMetadata metadata = new ArbitraryDataTransactionMetadata(chunkFilePath);
|
||||
metadata.setTitle(this.title);
|
||||
metadata.setDescription(this.description);
|
||||
metadata.setTags(this.tags);
|
||||
metadata.setCategory(this.category);
|
||||
metadata.setChunks(this.arbitraryDataFile.chunkHashList());
|
||||
metadata.write();
|
||||
|
||||
// Create an ArbitraryDataFile from the JSON file (we don't have a signature yet)
|
||||
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromPath(chunkFilePath, null);
|
||||
@@ -308,6 +330,20 @@ public class ArbitraryDataWriter {
|
||||
throw new DataException(String.format("Missing chunk %s in metadata file", Base58.encode(chunk)));
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the metadata is correct
|
||||
if (!Objects.equals(metadata.getTitle(), this.title)) {
|
||||
throw new DataException("Metadata mismatch: title");
|
||||
}
|
||||
if (!Objects.equals(metadata.getDescription(), this.description)) {
|
||||
throw new DataException("Metadata mismatch: description");
|
||||
}
|
||||
if (!Objects.equals(metadata.getTags(), this.tags)) {
|
||||
throw new DataException("Metadata mismatch: tags");
|
||||
}
|
||||
if (!Objects.equals(metadata.getCategory(), this.category)) {
|
||||
throw new DataException("Metadata mismatch: category");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +366,16 @@ public class ArbitraryDataWriter {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean needsMetadataFile() {
|
||||
if (this.arbitraryDataFile.chunkCount() > 1) {
|
||||
return true;
|
||||
}
|
||||
if (this.title != null || this.description != null || this.tags != null || this.category != null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public ArbitraryDataFile getArbitraryDataFile() {
|
||||
return this.arbitraryDataFile;
|
||||
|
||||
@@ -2,17 +2,28 @@ package org.qortal.arbitrary.metadata;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
|
||||
private List<byte[]> chunks;
|
||||
private String title;
|
||||
private String description;
|
||||
private List<String> tags;
|
||||
private Category category;
|
||||
|
||||
private static int MAX_TITLE_LENGTH = 80;
|
||||
private static int MAX_DESCRIPTION_LENGTH = 500;
|
||||
private static int MAX_TAG_LENGTH = 20;
|
||||
private static int MAX_TAGS_COUNT = 5;
|
||||
|
||||
public ArbitraryDataTransactionMetadata(Path filePath) {
|
||||
super(filePath);
|
||||
@@ -25,10 +36,37 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
throw new DataException("Transaction metadata JSON string is null");
|
||||
}
|
||||
|
||||
JSONObject metadata = new JSONObject(this.jsonString);
|
||||
|
||||
if (metadata.has("title")) {
|
||||
this.title = metadata.getString("title");
|
||||
}
|
||||
|
||||
if (metadata.has("description")) {
|
||||
this.description = metadata.getString("description");
|
||||
}
|
||||
|
||||
List<String> tagsList = new ArrayList<>();
|
||||
if (metadata.has("tags")) {
|
||||
JSONArray tags = metadata.getJSONArray("tags");
|
||||
if (tags != null) {
|
||||
for (int i=0; i<tags.length(); i++) {
|
||||
String tag = tags.getString(i);
|
||||
if (tag != null) {
|
||||
tagsList.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.tags = tagsList;
|
||||
}
|
||||
|
||||
if (metadata.has("category")) {
|
||||
this.category = Category.uncategorizedValueOf(metadata.getString("category"));
|
||||
}
|
||||
|
||||
List<byte[]> chunksList = new ArrayList<>();
|
||||
JSONObject cache = new JSONObject(this.jsonString);
|
||||
if (cache.has("chunks")) {
|
||||
JSONArray chunks = cache.getJSONArray("chunks");
|
||||
if (metadata.has("chunks")) {
|
||||
JSONArray chunks = metadata.getJSONArray("chunks");
|
||||
if (chunks != null) {
|
||||
for (int i=0; i<chunks.length(); i++) {
|
||||
String chunk = chunks.getString(i);
|
||||
@@ -45,6 +83,26 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
protected void buildJson() {
|
||||
JSONObject outer = new JSONObject();
|
||||
|
||||
if (this.title != null && !this.title.isEmpty()) {
|
||||
outer.put("title", this.title);
|
||||
}
|
||||
|
||||
if (this.description != null && !this.description.isEmpty()) {
|
||||
outer.put("description", this.description);
|
||||
}
|
||||
|
||||
JSONArray tags = new JSONArray();
|
||||
if (this.tags != null) {
|
||||
for (String tag : this.tags) {
|
||||
tags.put(tag);
|
||||
}
|
||||
outer.put("tags", tags);
|
||||
}
|
||||
|
||||
if (this.category != null) {
|
||||
outer.put("category", this.category.toString());
|
||||
}
|
||||
|
||||
JSONArray chunks = new JSONArray();
|
||||
if (this.chunks != null) {
|
||||
for (byte[] chunk : this.chunks) {
|
||||
@@ -66,6 +124,38 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
return this.chunks;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setTags(List<String> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public List<String> getTags() {
|
||||
return this.tags;
|
||||
}
|
||||
|
||||
public void setCategory(Category category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public Category getCategory() {
|
||||
return this.category;
|
||||
}
|
||||
|
||||
public boolean containsChunk(byte[] chunk) {
|
||||
for (byte[] c : this.chunks) {
|
||||
if (Arrays.equals(c, chunk)) {
|
||||
@@ -75,4 +165,61 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Static helper methods
|
||||
|
||||
public static String limitTitle(String title) {
|
||||
if (title == null) {
|
||||
return null;
|
||||
}
|
||||
if (title.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return title.substring(0, Math.min(title.length(), MAX_TITLE_LENGTH));
|
||||
}
|
||||
|
||||
public static String limitDescription(String description) {
|
||||
if (description == null) {
|
||||
return null;
|
||||
}
|
||||
if (description.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return description.substring(0, Math.min(description.length(), MAX_DESCRIPTION_LENGTH));
|
||||
}
|
||||
|
||||
public static List<String> limitTags(List<String> tags) {
|
||||
if (tags == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure tags list is mutable
|
||||
List<String> mutableTags = new ArrayList<>(tags);
|
||||
|
||||
int tagCount = mutableTags.size();
|
||||
if (tagCount == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove tags over the limit
|
||||
// This is cleaner than truncating, which results in malformed tags
|
||||
// Also remove tags that are empty
|
||||
Iterator iterator = mutableTags.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
String tag = (String) iterator.next();
|
||||
if (tag == null || tag.length() > MAX_TAG_LENGTH || tag.isEmpty()) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Limit the total number of tags
|
||||
if (tagCount > MAX_TAGS_COUNT) {
|
||||
mutableTags = mutableTags.subList(0, MAX_TAGS_COUNT);
|
||||
}
|
||||
|
||||
return mutableTags;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
81
src/main/java/org/qortal/arbitrary/misc/Category.java
Normal file
81
src/main/java/org/qortal/arbitrary/misc/Category.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package org.qortal.arbitrary.misc;
|
||||
|
||||
public enum Category {
|
||||
ART("Art and Design"),
|
||||
AUTOMOTIVE("Automotive"),
|
||||
BEAUTY("Beauty"),
|
||||
BOOKS("Books and Reference"),
|
||||
BUSINESS("Business"),
|
||||
COMMUNICATIONS("Communications"),
|
||||
CRYPTOCURRENCY("Cryptocurrency and Blockchain"),
|
||||
CULTURE("Culture"),
|
||||
DATING("Dating"),
|
||||
DESIGN("Design"),
|
||||
ENTERTAINMENT("Entertainment"),
|
||||
EVENTS("Events"),
|
||||
FAITH("Faith and Religion"),
|
||||
FASHION("Fashion"),
|
||||
FINANCE("Finance"),
|
||||
FOOD("Food and Drink"),
|
||||
GAMING("Gaming"),
|
||||
GEOGRAPHY("Geography"),
|
||||
HEALTH("Health"),
|
||||
HISTORY("History"),
|
||||
HOME("Home"),
|
||||
KNOWLEDGE("Knowledge Share"),
|
||||
LANGUAGE("Language"),
|
||||
LIFESTYLE("Lifestyle"),
|
||||
MANUFACTURING("Manufacturing"),
|
||||
MAPS("Maps and Navigation"),
|
||||
MUSIC("Music"),
|
||||
NEWS("News"),
|
||||
OTHER("Other"),
|
||||
PETS("Pets"),
|
||||
PHILOSOPHY("Philosophy"),
|
||||
PHOTOGRAPHY("Photography"),
|
||||
POLITICS("Politics"),
|
||||
PRODUCE("Products and Services"),
|
||||
PRODUCTIVITY("Productivity"),
|
||||
PSYCHOLOGY("Psychology"),
|
||||
QORTAL("Qortal"),
|
||||
SCIENCE("Science"),
|
||||
SELF_CARE("Self Care"),
|
||||
SELF_SUFFICIENCY("Self-Sufficiency and Homesteading"),
|
||||
SHOPPING("Shopping"),
|
||||
SOCIAL("Social"),
|
||||
SOFTWARE("Software"),
|
||||
SPIRITUALITY("Spirituality"),
|
||||
SPORTS("Sports"),
|
||||
STORYTELLING("Storytelling"),
|
||||
TECHNOLOGY("Technology"),
|
||||
TOOLS("Tools"),
|
||||
TRAVEL("Travel"),
|
||||
UNCATEGORIZED("Uncategorized"),
|
||||
VIDEO("Video"),
|
||||
WEATHER("Weather");
|
||||
|
||||
private final String name;
|
||||
|
||||
Category(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as valueOf() but with fallback to UNCATEGORIZED if there's no match
|
||||
* @param name
|
||||
* @return a Category (using UNCATEGORIZED if no match found)
|
||||
*/
|
||||
public static Category uncategorizedValueOf(String name) {
|
||||
try {
|
||||
return Category.valueOf(name);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return Category.UNCATEGORIZED;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -551,7 +551,7 @@ public class QortalATAPI extends API {
|
||||
* <p>
|
||||
* Otherwise, assume B is a public key.
|
||||
*/
|
||||
private Account getAccountFromB(MachineState state) {
|
||||
/*package*/ Account getAccountFromB(MachineState state) {
|
||||
byte[] bBytes = this.getB(state);
|
||||
|
||||
if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION)
|
||||
|
||||
@@ -10,9 +10,11 @@ import org.ciyam.at.ExecutionException;
|
||||
import org.ciyam.at.FunctionData;
|
||||
import org.ciyam.at.IllegalFunctionCodeException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.crosschain.Bitcoin;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
/**
|
||||
@@ -160,6 +162,68 @@ public enum QortalFunctionCode {
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
convertAddressInB(Crypto.ADDRESS_VERSION, state);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Returns account level of account in B.<br>
|
||||
* <tt>0x0520</tt><br>
|
||||
* B should contain either Qortal address or public key,<br>
|
||||
* e.g. as a result of calling function {@link org.ciyam.at.FunctionCode#PUT_ADDRESS_FROM_TX_IN_A_INTO_B}</code>.
|
||||
* <p></p>
|
||||
* Returns account level, or -1 if account unknown.
|
||||
* <p></p>
|
||||
* @see QortalATAPI#getAccountFromB(MachineState)
|
||||
*/
|
||||
GET_ACCOUNT_LEVEL_FROM_ACCOUNT_IN_B(0x0520, 0, true) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
Account account = api.getAccountFromB(state);
|
||||
|
||||
Integer accountLevel = null;
|
||||
|
||||
if (account != null) {
|
||||
try {
|
||||
accountLevel = account.getLevel();
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch account level?", e);
|
||||
}
|
||||
}
|
||||
|
||||
functionData.returnValue = accountLevel != null
|
||||
? accountLevel.longValue()
|
||||
: -1;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Returns account's minted block count of account in B.<br>
|
||||
* <tt>0x0521</tt><br>
|
||||
* B should contain either Qortal address or public key,<br>
|
||||
* e.g. as a result of calling function {@link org.ciyam.at.FunctionCode#PUT_ADDRESS_FROM_TX_IN_A_INTO_B}</code>.
|
||||
* <p></p>
|
||||
* Returns account level, or -1 if account unknown.
|
||||
* <p></p>
|
||||
* @see QortalATAPI#getAccountFromB(MachineState)
|
||||
*/
|
||||
GET_BLOCKS_MINTED_FROM_ACCOUNT_IN_B(0x0521, 0, true) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
Account account = api.getAccountFromB(state);
|
||||
|
||||
Integer blocksMinted = null;
|
||||
|
||||
if (account != null) {
|
||||
try {
|
||||
blocksMinted = account.getBlocksMinted();
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch account's minted block count?", e);
|
||||
}
|
||||
}
|
||||
|
||||
functionData.returnValue = blocksMinted != null
|
||||
? blocksMinted.longValue()
|
||||
: -1;
|
||||
}
|
||||
};
|
||||
|
||||
public final short value;
|
||||
|
||||
@@ -8,13 +8,7 @@ import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
@@ -28,7 +22,7 @@ import org.qortal.asset.Asset;
|
||||
import org.qortal.at.AT;
|
||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||
import org.qortal.block.BlockChain.AccountLevelShareBin;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.OnlineAccountsManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountBalanceData;
|
||||
import org.qortal.data.account.AccountData;
|
||||
@@ -320,7 +314,7 @@ public class Block {
|
||||
byte[] reference = parentBlockData.getSignature();
|
||||
|
||||
// Fetch our list of online accounts
|
||||
List<OnlineAccountData> onlineAccounts = Controller.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
if (onlineAccounts.isEmpty()) {
|
||||
LOGGER.error("No online accounts - not even our own?");
|
||||
return null;
|
||||
@@ -333,6 +327,11 @@ public class Block {
|
||||
onlineAccountsTimestamp = onlineAccountData.getTimestamp();
|
||||
}
|
||||
|
||||
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
|
||||
// This is up to 100x faster than querying each index separately. For 4150 reward share keys, it
|
||||
// was taking around 5000ms to query individually, vs 50ms using this approach.
|
||||
List<byte[]> allRewardSharePublicKeys = repository.getAccountRepository().getRewardSharePublicKeys();
|
||||
|
||||
// Map using index into sorted list of reward-shares as key
|
||||
Map<Integer, OnlineAccountData> indexedOnlineAccounts = new HashMap<>();
|
||||
for (OnlineAccountData onlineAccountData : onlineAccounts) {
|
||||
@@ -340,7 +339,7 @@ public class Block {
|
||||
if (onlineAccountData.getTimestamp() != onlineAccountsTimestamp)
|
||||
continue;
|
||||
|
||||
Integer accountIndex = repository.getAccountRepository().getRewardShareIndex(onlineAccountData.getPublicKey());
|
||||
Integer accountIndex = getRewardShareIndex(onlineAccountData.getPublicKey(), allRewardSharePublicKeys);
|
||||
if (accountIndex == null)
|
||||
// Online account (reward-share) with current timestamp but reward-share cancelled
|
||||
continue;
|
||||
@@ -988,10 +987,10 @@ public class Block {
|
||||
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() - Controller.ONLINE_TIMESTAMP_MODULUS
|
||||
List<OnlineAccountData> currentOnlineAccounts = onlineTimestamp < NTP.getTime() - OnlineAccountsManager.ONLINE_TIMESTAMP_MODULUS
|
||||
? null
|
||||
: Controller.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> latestBlocksOnlineAccounts = Controller.getInstance().getLatestBlocksOnlineAccounts();
|
||||
: OnlineAccountsManager.getInstance().getOnlineAccounts();
|
||||
List<OnlineAccountData> latestBlocksOnlineAccounts = OnlineAccountsManager.getInstance().getLatestBlocksOnlineAccounts();
|
||||
|
||||
// Extract online accounts' timestamp signatures from block data
|
||||
List<byte[]> onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getOnlineAccountsSignatures());
|
||||
@@ -1369,7 +1368,7 @@ public class Block {
|
||||
postBlockTidy();
|
||||
|
||||
// Give Controller our cached, valid online accounts data (if any) to help reduce CPU load for next block
|
||||
Controller.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
|
||||
OnlineAccountsManager.getInstance().pushLatestBlocksOnlineAccounts(this.cachedValidOnlineAccounts);
|
||||
|
||||
// Log some debugging info relating to the block weight calculation
|
||||
this.logDebugInfo();
|
||||
@@ -1588,7 +1587,7 @@ public class Block {
|
||||
postBlockTidy();
|
||||
|
||||
// Remove any cached, valid online accounts data from Controller
|
||||
Controller.getInstance().popLatestBlocksOnlineAccounts();
|
||||
OnlineAccountsManager.getInstance().popLatestBlocksOnlineAccounts();
|
||||
}
|
||||
|
||||
protected void orphanTransactionsFromBlock() throws DataException {
|
||||
@@ -2029,6 +2028,26 @@ public class Block {
|
||||
this.repository.getAccountRepository().tidy();
|
||||
}
|
||||
|
||||
// Utils
|
||||
|
||||
/**
|
||||
* Find index of rewardSharePublicKey in list of rewardSharePublicKeys
|
||||
*
|
||||
* @param rewardSharePublicKey - the key to query
|
||||
* @param rewardSharePublicKeys - a sorted list of keys
|
||||
* @return - the index of the key, or null if not found
|
||||
*/
|
||||
private static Integer getRewardShareIndex(byte[] rewardSharePublicKey, List<byte[]> rewardSharePublicKeys) {
|
||||
int index = 0;
|
||||
for (byte[] publicKey : rewardSharePublicKeys) {
|
||||
if (Arrays.equals(rewardSharePublicKey, publicKey)) {
|
||||
return index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void logDebugInfo() {
|
||||
try {
|
||||
// Avoid calculations if possible. We have to check against INFO here, since Level.isMoreSpecificThan() confusingly uses <= rather than just <
|
||||
|
||||
@@ -68,9 +68,19 @@ public class BlockChain {
|
||||
atFindNextTransactionFix,
|
||||
newBlockSigHeight,
|
||||
shareBinFix,
|
||||
calcChainWeightTimestamp;
|
||||
calcChainWeightTimestamp,
|
||||
transactionV5Timestamp;
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
/** Unit fees by transaction timestamp */
|
||||
public static class UnitFeesByTimestamp {
|
||||
public long timestamp;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
public long fee;
|
||||
}
|
||||
private List<UnitFeesByTimestamp> nameRegistrationUnitFees;
|
||||
|
||||
/** Map of which blockchain features are enabled when (height/timestamp) */
|
||||
@XmlJavaTypeAdapter(StringLongMapXmlAdapter.class)
|
||||
private Map<String, Long> featureTriggers;
|
||||
@@ -141,7 +151,8 @@ public class BlockChain {
|
||||
}
|
||||
private List<BlockTimingByHeight> blockTimingsByHeight;
|
||||
|
||||
private int minAccountLevelToMint = 1;
|
||||
private int minAccountLevelToMint;
|
||||
private int minAccountLevelForBlockSubmissions;
|
||||
private int minAccountLevelToRewardShare;
|
||||
private int maxRewardSharesPerMintingAccount;
|
||||
private int founderEffectiveMintingLevel;
|
||||
@@ -344,6 +355,10 @@ public class BlockChain {
|
||||
return this.minAccountLevelToMint;
|
||||
}
|
||||
|
||||
public int getMinAccountLevelForBlockSubmissions() {
|
||||
return this.minAccountLevelForBlockSubmissions;
|
||||
}
|
||||
|
||||
public int getMinAccountLevelToRewardShare() {
|
||||
return this.minAccountLevelToRewardShare;
|
||||
}
|
||||
@@ -386,6 +401,10 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.calcChainWeightTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getTransactionV5Timestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.transactionV5Timestamp.name()).longValue();
|
||||
}
|
||||
|
||||
// More complex getters for aspects that change by height or timestamp
|
||||
|
||||
public long getRewardAtHeight(int ourHeight) {
|
||||
@@ -405,6 +424,15 @@ public class BlockChain {
|
||||
throw new IllegalStateException(String.format("No block timing info available for height %d", ourHeight));
|
||||
}
|
||||
|
||||
public long getNameRegistrationUnitFeeAtTimestamp(long ourTimestamp) {
|
||||
for (int i = nameRegistrationUnitFees.size() - 1; i >= 0; --i)
|
||||
if (nameRegistrationUnitFees.get(i).timestamp <= ourTimestamp)
|
||||
return nameRegistrationUnitFees.get(i).fee;
|
||||
|
||||
// Default to system-wide unit fee
|
||||
return this.getUnitFee();
|
||||
}
|
||||
|
||||
/** Validate blockchain config read from JSON */
|
||||
private void validateConfig() {
|
||||
if (this.genesisInfo == null)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
@@ -16,11 +18,11 @@ import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.block.Block.ValidationResult;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.block.CommonBlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
@@ -48,11 +50,6 @@ public class BlockMinter extends Thread {
|
||||
// Recovery
|
||||
public static final long INVALID_BLOCK_RECOVERY_TIMEOUT = 10 * 60 * 1000L; // ms
|
||||
|
||||
// Min account level to submit blocks
|
||||
// This is an unvalidated version of Blockchain.minAccountLevelToMint
|
||||
// and exists only to reduce block candidates by default.
|
||||
private static int MIN_LEVEL_FOR_BLOCK_SUBMISSION = 6;
|
||||
|
||||
// Constructors
|
||||
|
||||
public BlockMinter() {
|
||||
@@ -81,6 +78,10 @@ public class BlockMinter extends Thread {
|
||||
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,
|
||||
@@ -109,7 +110,7 @@ public class BlockMinter extends Thread {
|
||||
continue;
|
||||
|
||||
// No online accounts? (e.g. during startup)
|
||||
if (Controller.getInstance().getOnlineAccounts().isEmpty())
|
||||
if (OnlineAccountsManager.getInstance().getOnlineAccounts().isEmpty())
|
||||
continue;
|
||||
|
||||
List<MintingAccountData> mintingAccountsData = repository.getAccountRepository().getMintingAccounts();
|
||||
@@ -137,18 +138,18 @@ public class BlockMinter extends Thread {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Optional (non-validated) prevention of block submissions below a defined level
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(mintingAccount.getAddress());
|
||||
if (accountData != null) {
|
||||
Integer level = accountData.getLevel();
|
||||
if (level != null && level < MIN_LEVEL_FOR_BLOCK_SUBMISSION) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
// 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
|
||||
@@ -156,7 +157,7 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// 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 (Controller.getInstance().getRecoveryMode() == false)
|
||||
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?
|
||||
@@ -181,7 +182,7 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// 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 (Controller.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
||||
if (Synchronizer.getInstance().getRecoveryMode() == false && recoverInvalidBlock == false)
|
||||
continue;
|
||||
|
||||
// There are enough peers with a recent block and our latest block is recent
|
||||
@@ -195,6 +196,9 @@ public class BlockMinter extends Thread {
|
||||
|
||||
// Reduce log timeout
|
||||
logTimeout = 10 * 1000L;
|
||||
|
||||
// Last low weight block is no longer valid
|
||||
parentSignatureForLastLowWeightBlock = null;
|
||||
}
|
||||
|
||||
// Discard accounts we have already built blocks with
|
||||
@@ -211,6 +215,14 @@ public class BlockMinter extends Thread {
|
||||
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()) {
|
||||
@@ -302,6 +314,44 @@ public class BlockMinter extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
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.debug("Higher weight chain found in peers, so not signing a block this round");
|
||||
LOGGER.debug("Time since detected: {}ms", NTP.getTime() - timeOfLastLowWeightBlock);
|
||||
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...");
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -429,7 +479,7 @@ public class BlockMinter extends Thread {
|
||||
throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
|
||||
|
||||
// Ensure mintingAccount is 'online' so blocks can be minted
|
||||
Controller.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
|
||||
OnlineAccountsManager.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
|
||||
|
||||
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||
|
||||
@@ -469,6 +519,61 @@ public class BlockMinter extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private BigInteger getOurChainWeightSinceBlock(Repository repository, BlockSummaryData commonBlock, List<BlockSummaryData> peerBlockSummaries) throws DataException {
|
||||
final int commonBlockHeight = commonBlock.getHeight();
|
||||
final byte[] commonBlockSig = commonBlock.getSignature();
|
||||
int mutualHeight = commonBlockHeight;
|
||||
|
||||
// Fetch our corresponding block summaries
|
||||
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
|
||||
List<BlockSummaryData> ourBlockSummaries = repository.getBlockRepository()
|
||||
.getBlockSummaries(commonBlockHeight + 1, ourLatestBlockData.getHeight());
|
||||
if (!ourBlockSummaries.isEmpty()) {
|
||||
Synchronizer.getInstance().populateBlockSummariesMinterLevels(repository, ourBlockSummaries);
|
||||
}
|
||||
|
||||
if (ourBlockSummaries != null && peerBlockSummaries != null) {
|
||||
mutualHeight += Math.min(ourBlockSummaries.size(), peerBlockSummaries.size());
|
||||
}
|
||||
return Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
||||
}
|
||||
|
||||
private boolean higherWeightChainExists(Repository repository, BigInteger blockCandidateWeight) throws DataException {
|
||||
if (blockCandidateWeight == null) {
|
||||
// Can't make decisions without knowing the block candidate weight
|
||||
return false;
|
||||
}
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
|
||||
List<Peer> peers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
// Loop through handshaked peers and check for any new block candidates
|
||||
for (Peer peer : peers) {
|
||||
if (peer.getCommonBlockData() != null && peer.getCommonBlockData().getCommonBlockSummary() != null) {
|
||||
// This peer has common block data
|
||||
CommonBlockData commonBlockData = peer.getCommonBlockData();
|
||||
BlockSummaryData commonBlockSummaryData = commonBlockData.getCommonBlockSummary();
|
||||
if (commonBlockData.getChainWeight() != 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;
|
||||
|
||||
} else {
|
||||
LOGGER.debug("Peer {} is on a lower weight chain ({}) than ours ({})", peer, formatter.format(peerChainWeight), formatter.format(ourChainWeight));
|
||||
}
|
||||
} else {
|
||||
LOGGER.debug("Peer {} has no chain weight", peer);
|
||||
}
|
||||
} else {
|
||||
LOGGER.debug("Peer {} has no common block data", peer);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private static void moderatedLog(Runnable logFunction) {
|
||||
// We only log if logging at TRACE or previous log timeout has expired
|
||||
if (!LOGGER.isTraceEnabled() && lastLogTimestamp != null && lastLogTimestamp + logTimeout > System.currentTimeMillis())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
524
src/main/java/org/qortal/controller/OnlineAccountsManager.java
Normal file
524
src/main/java/org/qortal/controller/OnlineAccountsManager.java
Normal file
@@ -0,0 +1,524 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.*;
|
||||
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;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class OnlineAccountsManager extends Thread {
|
||||
|
||||
private class OurOnlineAccountsThread extends Thread {
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
while (!isStopping) {
|
||||
Thread.sleep(10000L);
|
||||
|
||||
// Refresh our online accounts signatures?
|
||||
sendOurOnlineAccountsInfo();
|
||||
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall through to exit thread
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(OnlineAccountsManager.class);
|
||||
|
||||
private static OnlineAccountsManager instance;
|
||||
private volatile boolean isStopping = false;
|
||||
|
||||
// To do with online accounts list
|
||||
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
|
||||
private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 1 * 60 * 1000L; // ms
|
||||
public static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000L;
|
||||
private static final long LAST_SEEN_EXPIRY_PERIOD = (ONLINE_TIMESTAMP_MODULUS * 2) + (1 * 60 * 1000L);
|
||||
/** How many (latest) blocks' worth of online accounts we cache */
|
||||
private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 2;
|
||||
private static final long ONLINE_ACCOUNTS_V2_PEER_VERSION = 0x0300020000L;
|
||||
|
||||
private long onlineAccountsTasksTimestamp = Controller.startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
|
||||
|
||||
private final List<OnlineAccountData> onlineAccountsImportQueue = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
|
||||
/** Cache of current 'online accounts' */
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
|
||||
/** Cache of latest blocks' online accounts */
|
||||
Deque<List<OnlineAccountData>> latestBlocksOnlineAccounts = new ArrayDeque<>(MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS);
|
||||
|
||||
public OnlineAccountsManager() {
|
||||
|
||||
}
|
||||
|
||||
public static synchronized OnlineAccountsManager getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new OnlineAccountsManager();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
||||
// Start separate thread to prepare our online accounts
|
||||
// This could be converted to a thread pool later if more concurrency is needed
|
||||
OurOnlineAccountsThread ourOnlineAccountsThread = new OurOnlineAccountsThread();
|
||||
ourOnlineAccountsThread.start();
|
||||
|
||||
try {
|
||||
while (!Controller.isStopping()) {
|
||||
Thread.sleep(100L);
|
||||
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform tasks to do with managing online accounts list
|
||||
if (now >= onlineAccountsTasksTimestamp) {
|
||||
onlineAccountsTasksTimestamp = now + ONLINE_ACCOUNTS_TASKS_INTERVAL;
|
||||
performOnlineAccountsTasks();
|
||||
}
|
||||
|
||||
// Process queued online account verifications
|
||||
this.processOnlineAccountsImportQueue();
|
||||
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall through to exit thread
|
||||
}
|
||||
|
||||
ourOnlineAccountsThread.interrupt();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
isStopping = true;
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
|
||||
// Online accounts import queue
|
||||
|
||||
private void processOnlineAccountsImportQueue() {
|
||||
if (this.onlineAccountsImportQueue.isEmpty()) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Processing online accounts import queue (size: {})", this.onlineAccountsImportQueue.size());
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
List<OnlineAccountData> onlineAccountDataCopy = new ArrayList<>(this.onlineAccountsImportQueue);
|
||||
for (OnlineAccountData onlineAccountData : onlineAccountDataCopy) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.verifyAndAddAccount(repository, onlineAccountData);
|
||||
|
||||
// Remove from queue
|
||||
onlineAccountsImportQueue.remove(onlineAccountData);
|
||||
}
|
||||
|
||||
LOGGER.debug("Finished processing online accounts import queue");
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while verifying online accounts"), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Utilities
|
||||
|
||||
private void verifyAndAddAccount(Repository repository, OnlineAccountData onlineAccountData) throws DataException {
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
return;
|
||||
|
||||
PublicKeyAccount otherAccount = new PublicKeyAccount(repository, onlineAccountData.getPublicKey());
|
||||
|
||||
// Check timestamp is 'recent' here
|
||||
if (Math.abs(onlineAccountData.getTimestamp() - now) > ONLINE_TIMESTAMP_MODULUS * 2) {
|
||||
LOGGER.trace(() -> String.format("Rejecting online account %s with out of range timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify
|
||||
byte[] data = Longs.toByteArray(onlineAccountData.getTimestamp());
|
||||
if (!otherAccount.verify(onlineAccountData.getSignature(), data)) {
|
||||
LOGGER.trace(() -> String.format("Rejecting invalid online account %s", otherAccount.getAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Qortal: check online account is actually reward-share
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(onlineAccountData.getPublicKey());
|
||||
if (rewardShareData == null) {
|
||||
// Reward-share doesn't even exist - probably not a good sign
|
||||
LOGGER.trace(() -> String.format("Rejecting unknown online reward-share public key %s", Base58.encode(onlineAccountData.getPublicKey())));
|
||||
return;
|
||||
}
|
||||
|
||||
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
LOGGER.trace(() -> String.format("Rejecting online reward-share with non-minting account %s", mintingAccount.getAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this.onlineAccounts) {
|
||||
OnlineAccountData existingAccountData = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccountData.getPublicKey())).findFirst().orElse(null);
|
||||
|
||||
if (existingAccountData != null) {
|
||||
if (existingAccountData.getTimestamp() < onlineAccountData.getTimestamp()) {
|
||||
this.onlineAccounts.remove(existingAccountData);
|
||||
|
||||
LOGGER.trace(() -> String.format("Updated online account %s with timestamp %d (was %d)", otherAccount.getAddress(), onlineAccountData.getTimestamp(), existingAccountData.getTimestamp()));
|
||||
} else {
|
||||
LOGGER.trace(() -> String.format("Not updating existing online account %s", otherAccount.getAddress()));
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
LOGGER.trace(() -> String.format("Added online account %s with timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp()));
|
||||
}
|
||||
|
||||
this.onlineAccounts.add(onlineAccountData);
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureTestingAccountsOnline(PrivateKeyAccount... onlineAccounts) {
|
||||
if (!BlockChain.getInstance().isTestChain()) {
|
||||
LOGGER.warn("Ignoring attempt to ensure test account is online for non-test chain!");
|
||||
return;
|
||||
}
|
||||
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
return;
|
||||
|
||||
final long onlineAccountsTimestamp = toOnlineAccountTimestamp(now);
|
||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||
|
||||
synchronized (this.onlineAccounts) {
|
||||
this.onlineAccounts.clear();
|
||||
|
||||
for (PrivateKeyAccount onlineAccount : onlineAccounts) {
|
||||
// Check mintingAccount is actually reward-share?
|
||||
|
||||
byte[] signature = onlineAccount.sign(timestampBytes);
|
||||
byte[] publicKey = onlineAccount.getPublicKey();
|
||||
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
||||
this.onlineAccounts.add(ourOnlineAccountData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void performOnlineAccountsTasks() {
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null)
|
||||
return;
|
||||
|
||||
// Expire old entries
|
||||
final long cutoffThreshold = now - LAST_SEEN_EXPIRY_PERIOD;
|
||||
synchronized (this.onlineAccounts) {
|
||||
Iterator<OnlineAccountData> iterator = this.onlineAccounts.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccountData onlineAccountData = iterator.next();
|
||||
|
||||
if (onlineAccountData.getTimestamp() < cutoffThreshold) {
|
||||
iterator.remove();
|
||||
|
||||
LOGGER.trace(() -> {
|
||||
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccountData.getPublicKey());
|
||||
return String.format("Removed expired online account %s with timestamp %d", otherAccount.getAddress(), onlineAccountData.getTimestamp());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request data from other peers?
|
||||
if ((this.onlineAccountsTasksTimestamp % ONLINE_ACCOUNTS_BROADCAST_INTERVAL) < ONLINE_ACCOUNTS_TASKS_INTERVAL) {
|
||||
List<OnlineAccountData> safeOnlineAccounts;
|
||||
synchronized (this.onlineAccounts) {
|
||||
safeOnlineAccounts = new ArrayList<>(this.onlineAccounts);
|
||||
}
|
||||
|
||||
Message messageV1 = new GetOnlineAccountsMessage(safeOnlineAccounts);
|
||||
Message messageV2 = new GetOnlineAccountsV2Message(safeOnlineAccounts);
|
||||
|
||||
Network.getInstance().broadcast(peer ->
|
||||
peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION ? messageV2 : messageV1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendOurOnlineAccountsInfo() {
|
||||
final Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<MintingAccountData> mintingAccounts;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
mintingAccounts = repository.getAccountRepository().getMintingAccounts();
|
||||
|
||||
// We have no accounts, but don't reset timestamp
|
||||
if (mintingAccounts.isEmpty())
|
||||
return;
|
||||
|
||||
// Only reward-share accounts allowed
|
||||
Iterator<MintingAccountData> iterator = mintingAccounts.iterator();
|
||||
int i = 0;
|
||||
while (iterator.hasNext()) {
|
||||
MintingAccountData mintingAccountData = iterator.next();
|
||||
|
||||
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(mintingAccountData.getPublicKey());
|
||||
if (rewardShareData == null) {
|
||||
// Reward-share doesn't even exist - probably not a good sign
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
|
||||
if (!mintingAccount.canMint()) {
|
||||
// Minting-account component of reward-share can no longer mint - disregard
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (++i > 1+1) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue trying to fetch minting accounts: %s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 'current' timestamp
|
||||
final long onlineAccountsTimestamp = toOnlineAccountTimestamp(now);
|
||||
boolean hasInfoChanged = false;
|
||||
|
||||
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
|
||||
List<OnlineAccountData> ourOnlineAccounts = new ArrayList<>();
|
||||
|
||||
MINTING_ACCOUNTS:
|
||||
for (MintingAccountData mintingAccountData : mintingAccounts) {
|
||||
PrivateKeyAccount mintingAccount = new PrivateKeyAccount(null, mintingAccountData.getPrivateKey());
|
||||
|
||||
byte[] signature = mintingAccount.sign(timestampBytes);
|
||||
byte[] publicKey = mintingAccount.getPublicKey();
|
||||
|
||||
// Our account is online
|
||||
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey);
|
||||
synchronized (this.onlineAccounts) {
|
||||
Iterator<OnlineAccountData> iterator = this.onlineAccounts.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccountData existingOnlineAccountData = iterator.next();
|
||||
|
||||
if (Arrays.equals(existingOnlineAccountData.getPublicKey(), ourOnlineAccountData.getPublicKey())) {
|
||||
// If our online account is already present, with same timestamp, then move on to next mintingAccount
|
||||
if (existingOnlineAccountData.getTimestamp() == onlineAccountsTimestamp)
|
||||
continue MINTING_ACCOUNTS;
|
||||
|
||||
// If our online account is already present, but with older timestamp, then remove it
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.onlineAccounts.add(ourOnlineAccountData);
|
||||
}
|
||||
|
||||
LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", mintingAccount.getAddress(), onlineAccountsTimestamp));
|
||||
ourOnlineAccounts.add(ourOnlineAccountData);
|
||||
hasInfoChanged = true;
|
||||
}
|
||||
|
||||
if (!hasInfoChanged)
|
||||
return;
|
||||
|
||||
Message messageV1 = new OnlineAccountsMessage(ourOnlineAccounts);
|
||||
Message messageV2 = new OnlineAccountsV2Message(ourOnlineAccounts);
|
||||
|
||||
Network.getInstance().broadcast(peer ->
|
||||
peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION ? messageV2 : messageV1
|
||||
);
|
||||
|
||||
LOGGER.trace(() -> String.format("Broadcasted %d online account%s with timestamp %d", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp));
|
||||
}
|
||||
|
||||
public static long toOnlineAccountTimestamp(long timestamp) {
|
||||
return (timestamp / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS;
|
||||
}
|
||||
|
||||
/** Returns list of online accounts with timestamp recent enough to be considered currently online. */
|
||||
public List<OnlineAccountData> getOnlineAccounts() {
|
||||
final long onlineTimestamp = toOnlineAccountTimestamp(NTP.getTime());
|
||||
|
||||
synchronized (this.onlineAccounts) {
|
||||
return this.onlineAccounts.stream().filter(account -> account.getTimestamp() == onlineTimestamp).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Returns cached, unmodifiable list of latest block's online accounts. */
|
||||
public List<OnlineAccountData> getLatestBlocksOnlineAccounts() {
|
||||
synchronized (this.latestBlocksOnlineAccounts) {
|
||||
return this.latestBlocksOnlineAccounts.peekFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/** Caches list of latest block's online accounts. Typically called by Block.process() */
|
||||
public void pushLatestBlocksOnlineAccounts(List<OnlineAccountData> latestBlocksOnlineAccounts) {
|
||||
synchronized (this.latestBlocksOnlineAccounts) {
|
||||
if (this.latestBlocksOnlineAccounts.size() == MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS)
|
||||
this.latestBlocksOnlineAccounts.pollLast();
|
||||
|
||||
this.latestBlocksOnlineAccounts.addFirst(latestBlocksOnlineAccounts == null
|
||||
? Collections.emptyList()
|
||||
: Collections.unmodifiableList(latestBlocksOnlineAccounts));
|
||||
}
|
||||
}
|
||||
|
||||
/** Reverts list of latest block's online accounts. Typically called by Block.orphan() */
|
||||
public void popLatestBlocksOnlineAccounts() {
|
||||
synchronized (this.latestBlocksOnlineAccounts) {
|
||||
this.latestBlocksOnlineAccounts.pollFirst();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Network handlers
|
||||
|
||||
public void onNetworkGetOnlineAccountsMessage(Peer peer, Message message) {
|
||||
GetOnlineAccountsMessage getOnlineAccountsMessage = (GetOnlineAccountsMessage) message;
|
||||
|
||||
List<OnlineAccountData> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
|
||||
|
||||
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
|
||||
List<OnlineAccountData> accountsToSend;
|
||||
synchronized (this.onlineAccounts) {
|
||||
accountsToSend = new ArrayList<>(this.onlineAccounts);
|
||||
}
|
||||
|
||||
Iterator<OnlineAccountData> iterator = accountsToSend.iterator();
|
||||
|
||||
SEND_ITERATOR:
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccountData onlineAccountData = iterator.next();
|
||||
|
||||
for (int i = 0; i < excludeAccounts.size(); ++i) {
|
||||
OnlineAccountData excludeAccountData = excludeAccounts.get(i);
|
||||
|
||||
if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
|
||||
iterator.remove();
|
||||
continue SEND_ITERATOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message onlineAccountsMessage = new OnlineAccountsMessage(accountsToSend);
|
||||
peer.sendMessage(onlineAccountsMessage);
|
||||
|
||||
LOGGER.trace(() -> String.format("Sent %d of our %d online accounts to %s", accountsToSend.size(), this.onlineAccounts.size(), peer));
|
||||
}
|
||||
|
||||
public void onNetworkOnlineAccountsMessage(Peer peer, Message message) {
|
||||
OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message;
|
||||
|
||||
List<OnlineAccountData> peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
LOGGER.trace(() -> String.format("Received %d online accounts from %s", peersOnlineAccounts.size(), peer));
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (OnlineAccountData onlineAccountData : peersOnlineAccounts)
|
||||
this.verifyAndAddAccount(repository, onlineAccountData);
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while verifying online accounts from peer %s", peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void onNetworkGetOnlineAccountsV2Message(Peer peer, Message message) {
|
||||
GetOnlineAccountsV2Message getOnlineAccountsMessage = (GetOnlineAccountsV2Message) message;
|
||||
|
||||
List<OnlineAccountData> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
|
||||
|
||||
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
|
||||
List<OnlineAccountData> accountsToSend;
|
||||
synchronized (this.onlineAccounts) {
|
||||
accountsToSend = new ArrayList<>(this.onlineAccounts);
|
||||
}
|
||||
|
||||
Iterator<OnlineAccountData> iterator = accountsToSend.iterator();
|
||||
|
||||
SEND_ITERATOR:
|
||||
while (iterator.hasNext()) {
|
||||
OnlineAccountData onlineAccountData = iterator.next();
|
||||
|
||||
for (int i = 0; i < excludeAccounts.size(); ++i) {
|
||||
OnlineAccountData excludeAccountData = excludeAccounts.get(i);
|
||||
|
||||
if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
|
||||
iterator.remove();
|
||||
continue SEND_ITERATOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message onlineAccountsMessage = new OnlineAccountsV2Message(accountsToSend);
|
||||
peer.sendMessage(onlineAccountsMessage);
|
||||
|
||||
LOGGER.trace(() -> String.format("Sent %d of our %d online accounts to %s", accountsToSend.size(), this.onlineAccounts.size(), peer));
|
||||
}
|
||||
|
||||
public void onNetworkOnlineAccountsV2Message(Peer peer, Message message) {
|
||||
OnlineAccountsV2Message onlineAccountsMessage = (OnlineAccountsV2Message) message;
|
||||
|
||||
List<OnlineAccountData> peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
|
||||
LOGGER.debug(String.format("Received %d online accounts from %s", peersOnlineAccounts.size(), peer));
|
||||
|
||||
int importCount = 0;
|
||||
|
||||
// Add any online accounts to the queue that aren't already present
|
||||
for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
|
||||
|
||||
// Do we already know about this online account data?
|
||||
if (onlineAccounts.contains(onlineAccountData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is it already in the import queue?
|
||||
if (onlineAccountsImportQueue.contains(onlineAccountData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
onlineAccountsImportQueue.add(onlineAccountData);
|
||||
importCount++;
|
||||
}
|
||||
|
||||
LOGGER.debug(String.format("Added %d online accounts to queue", importCount));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -20,6 +22,9 @@ 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;
|
||||
@@ -28,18 +33,17 @@ 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.Message.MessageType;
|
||||
import org.qortal.network.message.MessageType;
|
||||
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.utils.Base58;
|
||||
import org.qortal.utils.ByteArray;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import static org.qortal.network.Peer.FETCH_BLOCKS_TIMEOUT;
|
||||
|
||||
public class Synchronizer {
|
||||
public class Synchronizer extends Thread {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(Synchronizer.class);
|
||||
|
||||
@@ -57,12 +61,32 @@ public class Synchronizer {
|
||||
/** 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
|
||||
|
||||
|
||||
private boolean running;
|
||||
|
||||
/** Latest block signatures from other peers that we know are on inferior chains. */
|
||||
List<ByteArray> inferiorChainSignatures = new ArrayList<>();
|
||||
|
||||
/** Recovery mode, which is used to bring back a stalled network */
|
||||
private boolean recoveryMode = false;
|
||||
private boolean peersAvailable = true; // peersAvailable must default to true
|
||||
private long timePeersLastAvailable = 0;
|
||||
|
||||
// Keep track of the size of the last re-org, so it can be logged
|
||||
private int lastReorgSize;
|
||||
|
||||
/** Synchronization object for sync variables below */
|
||||
public final Object syncLock = new Object();
|
||||
/** Whether we are attempting to synchronize. */
|
||||
private volatile boolean isSynchronizing = false;
|
||||
/** Temporary estimate of synchronization progress for SysTray use. */
|
||||
private volatile int syncPercent = 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<>());
|
||||
public Long timeValidBlockLastReceived = null;
|
||||
@@ -71,12 +95,31 @@ public class Synchronizer {
|
||||
private static Synchronizer instance;
|
||||
|
||||
public enum SynchronizationResult {
|
||||
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE, SHUTTING_DOWN;
|
||||
OK, NOTHING_TO_DO, GENESIS_ONLY, NO_COMMON_BLOCK, TOO_DIVERGENT, NO_REPLY, INFERIOR_CHAIN, INVALID_DATA, NO_BLOCKCHAIN_LOCK, REPOSITORY_ISSUE, SHUTTING_DOWN, CHAIN_TIP_TOO_OLD;
|
||||
}
|
||||
|
||||
public static class NewChainTipEvent implements Event {
|
||||
private final BlockData priorChainTip;
|
||||
private final BlockData newChainTip;
|
||||
|
||||
public NewChainTipEvent(BlockData priorChainTip, BlockData newChainTip) {
|
||||
this.priorChainTip = priorChainTip;
|
||||
this.newChainTip = newChainTip;
|
||||
}
|
||||
|
||||
public BlockData getPriorChainTip() {
|
||||
return this.priorChainTip;
|
||||
}
|
||||
|
||||
public BlockData getNewChainTip() {
|
||||
return this.newChainTip;
|
||||
}
|
||||
}
|
||||
|
||||
// Constructors
|
||||
|
||||
private Synchronizer() {
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
public static Synchronizer getInstance() {
|
||||
@@ -87,6 +130,301 @@ public class Synchronizer {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Synchronizer");
|
||||
|
||||
try {
|
||||
while (running && !Controller.isStopping()) {
|
||||
Thread.sleep(1000);
|
||||
|
||||
if (requestSync) {
|
||||
requestSync = false;
|
||||
boolean success = Synchronizer.getInstance().potentiallySynchronize();
|
||||
if (!success) {
|
||||
// Something went wrong, so try again next time
|
||||
requestSync = true;
|
||||
}
|
||||
// Remember that we have a pending sync request if this attempt failed
|
||||
syncRequestPending = !success;
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Clear interrupted flag so we can shutdown trim threads
|
||||
Thread.interrupted();
|
||||
// Fall-through to exit
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.running = false;
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean isSynchronizing() {
|
||||
return this.isSynchronizing;
|
||||
}
|
||||
|
||||
public boolean isSyncRequestPending() {
|
||||
return this.syncRequestPending;
|
||||
}
|
||||
|
||||
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);
|
||||
if (Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return this.isSynchronizing ? this.syncPercent : null;
|
||||
}
|
||||
}
|
||||
|
||||
public void requestSync() {
|
||||
requestSync = true;
|
||||
}
|
||||
|
||||
public boolean isSyncRequested() {
|
||||
return requestSync;
|
||||
}
|
||||
|
||||
public boolean getRecoveryMode() {
|
||||
return this.recoveryMode;
|
||||
}
|
||||
|
||||
|
||||
public boolean potentiallySynchronize() throws InterruptedException {
|
||||
// Already synchronizing via another thread?
|
||||
if (this.isSynchronizing)
|
||||
return true;
|
||||
|
||||
// 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
|
||||
peers.removeIf(Controller.hasOnlyGenesisBlock);
|
||||
|
||||
// Disregard peers that don't have a recent block
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
|
||||
// Disregard peers that are on an old version
|
||||
peers.removeIf(Controller.hasOldVersion);
|
||||
|
||||
checkRecoveryModeForPeers(peers);
|
||||
if (recoveryMode) {
|
||||
// Needs a mutable copy of the unmodifiableList
|
||||
peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||
peers.removeIf(Controller.hasOnlyGenesisBlock);
|
||||
peers.removeIf(Controller.hasMisbehaved);
|
||||
peers.removeIf(Controller.hasOldVersion);
|
||||
}
|
||||
|
||||
// Check we have enough peers to potentially synchronize
|
||||
if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
|
||||
return true;
|
||||
|
||||
// Disregard peers that have no block signature or the same block signature as us
|
||||
peers.removeIf(Controller.hasNoOrSameBlock);
|
||||
|
||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||
peers.removeIf(Controller.hasInferiorChainTip);
|
||||
|
||||
final int peersBeforeComparison = peers.size();
|
||||
|
||||
// Request recent block summaries from the remaining peers, and locate our common block with each
|
||||
Synchronizer.getInstance().findCommonBlocksWithPeers(peers);
|
||||
|
||||
// Compare the peers against each other, and against our chain, which will return an updated list excluding those without common blocks
|
||||
peers = Synchronizer.getInstance().comparePeers(peers);
|
||||
|
||||
// We may have added more inferior chain tips when comparing peers, so remove any peers that are currently on those chains
|
||||
peers.removeIf(Controller.hasInferiorChainTip);
|
||||
|
||||
// Remove any peers that are no longer on a recent block since the last check
|
||||
// Except for times when we're in recovery mode, in which case we need to keep them
|
||||
if (!recoveryMode) {
|
||||
peers.removeIf(Controller.hasNoRecentBlock);
|
||||
}
|
||||
|
||||
final int peersRemoved = peersBeforeComparison - peers.size();
|
||||
if (peersRemoved > 0 && peers.size() > 0)
|
||||
LOGGER.debug(String.format("Ignoring %d peers on inferior chains. Peers remaining: %d", peersRemoved, peers.size()));
|
||||
|
||||
if (peers.isEmpty())
|
||||
return true;
|
||||
|
||||
if (peers.size() > 1) {
|
||||
StringBuilder finalPeersString = new StringBuilder();
|
||||
for (Peer peer : peers)
|
||||
finalPeersString = finalPeersString.length() > 0 ? finalPeersString.append(", ").append(peer) : finalPeersString.append(peer);
|
||||
LOGGER.debug(String.format("Choosing random peer from: [%s]", finalPeersString.toString()));
|
||||
}
|
||||
|
||||
// Pick random peer to sync with
|
||||
int index = new SecureRandom().nextInt(peers.size());
|
||||
Peer peer = peers.get(index);
|
||||
|
||||
SynchronizationResult syncResult = actuallySynchronize(peer, false);
|
||||
if (syncResult == SynchronizationResult.NO_BLOCKCHAIN_LOCK) {
|
||||
// No blockchain lock - force a retry by returning false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
|
||||
boolean hasStatusChanged = false;
|
||||
BlockData priorChainTip = Controller.getInstance().getChainTip();
|
||||
|
||||
synchronized (this.syncLock) {
|
||||
this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
|
||||
|
||||
// Only update SysTray if we're potentially changing height
|
||||
if (this.syncPercent < 100) {
|
||||
this.isSynchronizing = true;
|
||||
hasStatusChanged = true;
|
||||
}
|
||||
}
|
||||
peer.setSyncInProgress(true);
|
||||
|
||||
if (hasStatusChanged)
|
||||
Controller.getInstance().updateSysTray();
|
||||
|
||||
try {
|
||||
SynchronizationResult syncResult = Synchronizer.getInstance().synchronize(peer, force);
|
||||
switch (syncResult) {
|
||||
case GENESIS_ONLY:
|
||||
case NO_COMMON_BLOCK:
|
||||
case TOO_DIVERGENT:
|
||||
case INVALID_DATA: {
|
||||
// These are more serious results that warrant a cool-off
|
||||
LOGGER.info(String.format("Failed to synchronize with peer %s (%s) - cooling off", peer, syncResult.name()));
|
||||
|
||||
// Don't use this peer again for a while
|
||||
Network.getInstance().peerMisbehaved(peer);
|
||||
break;
|
||||
}
|
||||
|
||||
case INFERIOR_CHAIN: {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
// These are minor failure results so fine to try again
|
||||
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)))
|
||||
peer.disconnect("failed to notify peer of our superior chain");
|
||||
break;
|
||||
}
|
||||
|
||||
case NO_REPLY:
|
||||
case NO_BLOCKCHAIN_LOCK:
|
||||
case REPOSITORY_ISSUE:
|
||||
case CHAIN_TIP_TOO_OLD:
|
||||
// These are minor failure results so fine to try again
|
||||
LOGGER.debug(() -> String.format("Failed to synchronize with peer %s (%s)", peer, syncResult.name()));
|
||||
break;
|
||||
|
||||
case SHUTTING_DOWN:
|
||||
// Just quietly exit
|
||||
break;
|
||||
|
||||
case OK:
|
||||
// fall-through...
|
||||
case NOTHING_TO_DO: {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
|
||||
LOGGER.debug(() -> String.format("Synchronized with peer %s (%s)", peer, syncResult.name()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!running) {
|
||||
// We've stopped
|
||||
return SynchronizationResult.SHUTTING_DOWN;
|
||||
}
|
||||
|
||||
// Has our chain tip changed?
|
||||
BlockData newChainTip;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
newChainTip = repository.getBlockRepository().getLastBlock();
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn(String.format("Repository issue when trying to fetch post-synchronization chain tip: %s", e.getMessage()));
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
if (!Arrays.equals(newChainTip.getSignature(), priorChainTip.getSignature())) {
|
||||
// Reset our cache of inferior chains
|
||||
inferiorChainSignatures.clear();
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
|
||||
|
||||
EventBus.INSTANCE.notify(new NewChainTipEvent(priorChainTip, newChainTip));
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
} finally {
|
||||
this.isSynchronizing = false;
|
||||
peer.setSyncInProgress(false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkRecoveryModeForPeers(List<Peer> qualifiedPeers) {
|
||||
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
|
||||
if (handshakedPeers.size() > 0) {
|
||||
// There is at least one handshaked peer
|
||||
if (qualifiedPeers.isEmpty()) {
|
||||
// There are no 'qualified' peers - i.e. peers that have a recent block we can sync to
|
||||
boolean werePeersAvailable = peersAvailable;
|
||||
peersAvailable = false;
|
||||
|
||||
// If peers only just became unavailable, update our record of the time they were last available
|
||||
if (werePeersAvailable)
|
||||
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) {
|
||||
if (recoveryMode == false) {
|
||||
LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", RECOVERY_MODE_TIMEOUT/60/1000));
|
||||
recoveryMode = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We now have at least one peer with a recent block, so we can exit recovery mode and sync normally
|
||||
peersAvailable = true;
|
||||
if (recoveryMode) {
|
||||
LOGGER.info("Peers have become available again. Exiting recovery mode...");
|
||||
recoveryMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return recoveryMode;
|
||||
}
|
||||
|
||||
public void addInferiorChainSignature(byte[] inferiorSignature) {
|
||||
// Update our list of inferior chain tips
|
||||
ByteArray inferiorChainSignature = ByteArray.wrap(inferiorSignature);
|
||||
if (!inferiorChainSignatures.contains(inferiorChainSignature))
|
||||
inferiorChainSignatures.add(inferiorChainSignature);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterate through a list of supplied peers, and attempt to find our common block with each.
|
||||
* If a common block is found, its summary will be retained in the peer's commonBlockSummary property, for processing later.
|
||||
@@ -232,7 +570,7 @@ public class Synchronizer {
|
||||
// If our latest block is very old, it's best that we don't try and determine the best peers to sync to.
|
||||
// This is because it can involve very large chain comparisons, which is too intensive.
|
||||
// In reality, most forking problems occur near the chain tips, so we will reserve this functionality for those situations.
|
||||
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (minLatestBlockTimestamp == null)
|
||||
return peers;
|
||||
|
||||
@@ -259,6 +597,8 @@ public class Synchronizer {
|
||||
// Create a placeholder to track of common blocks that we can discard due to being inferior chains
|
||||
int dropPeersAfterCommonBlockHeight = 0;
|
||||
|
||||
NumberFormat accurateFormatter = new DecimalFormat("0.################E0");
|
||||
|
||||
// Remove peers with no common block data
|
||||
Iterator iterator = peers.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
@@ -279,7 +619,7 @@ public class Synchronizer {
|
||||
// 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));
|
||||
Controller.getInstance().addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
|
||||
this.addInferiorChainSignature(peer.getChainTipData().getLastBlockSignature());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -381,13 +721,12 @@ public class Synchronizer {
|
||||
if (ourBlockSummaries.size() > 0)
|
||||
ourChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), ourBlockSummaries, maxHeightForChainWeightComparisons);
|
||||
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
NumberFormat accurateFormatter = new DecimalFormat("0.################E0");
|
||||
LOGGER.debug(String.format("Our chain weight based on %d blocks is %s", (usingSameLengthChainWeight ? minChainLength : ourBlockSummaries.size()), formatter.format(ourChainWeight)));
|
||||
LOGGER.debug(String.format("Our chain weight based on %d blocks is %s", (usingSameLengthChainWeight ? minChainLength : ourBlockSummaries.size()), accurateFormatter.format(ourChainWeight)));
|
||||
|
||||
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();
|
||||
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
|
||||
final CommonBlockData peerCommonBlockData = peer.getCommonBlockData();
|
||||
|
||||
@@ -398,6 +737,14 @@ public class Synchronizer {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If peer is our of date (since our last check), we should exclude it from this round
|
||||
minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
|
||||
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {
|
||||
LOGGER.debug(String.format("Peer %s is out of date - removing it from this round", peer));
|
||||
peers.remove(peer);
|
||||
continue;
|
||||
}
|
||||
|
||||
final List<BlockSummaryData> peerBlockSummariesAfterCommonBlock = peerCommonBlockData.getBlockSummariesAfterCommonBlock();
|
||||
populateBlockSummariesMinterLevels(repository, peerBlockSummariesAfterCommonBlock);
|
||||
|
||||
@@ -405,7 +752,7 @@ public class Synchronizer {
|
||||
LOGGER.debug(String.format("About to calculate chain weight based on %d blocks for peer %s with common block %.8s (peer has %d blocks after common block)", (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peer, Base58.encode(commonBlockSummary.getSignature()), peerAdditionalBlocksAfterCommonBlock));
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockSummary.getHeight(), commonBlockSummary.getSignature(), peerBlockSummariesAfterCommonBlock, maxHeightForChainWeightComparisons);
|
||||
peer.getCommonBlockData().setChainWeight(peerChainWeight);
|
||||
LOGGER.debug(String.format("Chain weight of peer %s based on %d blocks (%d - %d) is %s", peer, (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peerBlockSummariesAfterCommonBlock.get(0).getHeight(), peerBlockSummariesAfterCommonBlock.get(peerBlockSummariesAfterCommonBlock.size()-1).getHeight(), formatter.format(peerChainWeight)));
|
||||
LOGGER.debug(String.format("Chain weight of peer %s based on %d blocks (%d - %d) is %s", peer, (usingSameLengthChainWeight ? minChainLength : peerBlockSummariesAfterCommonBlock.size()), peerBlockSummariesAfterCommonBlock.get(0).getHeight(), peerBlockSummariesAfterCommonBlock.get(peerBlockSummariesAfterCommonBlock.size()-1).getHeight(), accurateFormatter.format(peerChainWeight)));
|
||||
|
||||
// Compare against our chain - if our blockchain has greater weight then don't synchronize with peer (or any others in this group)
|
||||
if (ourChainWeight.compareTo(peerChainWeight) > 0) {
|
||||
@@ -571,9 +918,11 @@ public class Synchronizer {
|
||||
// Make sure we're the only thread modifying the blockchain
|
||||
// If we're already synchronizing with another peer then this will also return fast
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
if (!blockchainLock.tryLock())
|
||||
if (!blockchainLock.tryLock(3, TimeUnit.SECONDS)) {
|
||||
// Wasn't peer's fault we couldn't sync
|
||||
LOGGER.info("Synchronizer couldn't acquire blockchain lock");
|
||||
return SynchronizationResult.NO_BLOCKCHAIN_LOCK;
|
||||
}
|
||||
|
||||
try {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -793,7 +1142,7 @@ public class Synchronizer {
|
||||
return SynchronizationResult.REPOSITORY_ISSUE;
|
||||
|
||||
if (ourLatestBlockData.getTimestamp() < minLatestBlockTimestamp) {
|
||||
LOGGER.info(String.format("Ditching our chain after height %d as our latest block is very old", commonBlockHeight));
|
||||
LOGGER.info(String.format("Ditching our chain after height %d", commonBlockHeight));
|
||||
} else {
|
||||
// Compare chain weights
|
||||
|
||||
@@ -853,8 +1202,9 @@ public class Synchronizer {
|
||||
BigInteger ourChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, ourBlockSummaries, mutualHeight);
|
||||
BigInteger peerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockSig, peerBlockSummaries, mutualHeight);
|
||||
|
||||
NumberFormat formatter = new DecimalFormat("0.###E0");
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", formatter.format(ourChainWeight), formatter.format(peerChainWeight)));
|
||||
NumberFormat accurateFormatter = new DecimalFormat("0.################E0");
|
||||
LOGGER.debug(String.format("commonBlockHeight: %d, commonBlockSig: %.8s, ourBlockSummaries.size(): %d, peerBlockSummaries.size(): %d", commonBlockHeight, Base58.encode(commonBlockSig), ourBlockSummaries.size(), peerBlockSummaries.size()));
|
||||
LOGGER.debug(String.format("Our chain weight: %s, peer's chain weight: %s (higher is better)", accurateFormatter.format(ourChainWeight), accurateFormatter.format(peerChainWeight)));
|
||||
|
||||
// If our blockchain has greater weight then don't synchronize with peer
|
||||
if (ourChainWeight.compareTo(peerChainWeight) >= 0) {
|
||||
@@ -957,6 +1307,16 @@ public class Synchronizer {
|
||||
return SynchronizationResult.INVALID_DATA;
|
||||
}
|
||||
|
||||
// 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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
byte[] nextPeerSignature = peerBlockSignatures.get(0);
|
||||
int nextHeight = height + 1;
|
||||
|
||||
@@ -1222,7 +1582,7 @@ public class Synchronizer {
|
||||
return new Block(repository, blockMessage.getBlockData(), blockMessage.getTransactions(), blockMessage.getAtStates());
|
||||
}
|
||||
|
||||
private void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
|
||||
public void populateBlockSummariesMinterLevels(Repository repository, List<BlockSummaryData> blockSummaries) throws DataException {
|
||||
final int firstBlockHeight = blockSummaries.get(0).getHeight();
|
||||
|
||||
for (int i = 0; i < blockSummaries.size(); ++i) {
|
||||
|
||||
357
src/main/java/org/qortal/controller/TransactionImporter.java
Normal file
357
src/main/java/org/qortal/controller/TransactionImporter.java
Normal file
@@ -0,0 +1,357 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.GetTransactionMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.TransactionMessage;
|
||||
import org.qortal.network.message.TransactionSignaturesMessage;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
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;
|
||||
|
||||
public class TransactionImporter extends Thread {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(TransactionImporter.class);
|
||||
|
||||
private static TransactionImporter instance;
|
||||
private volatile boolean isStopping = false;
|
||||
|
||||
private static final int MAX_INCOMING_TRANSACTIONS = 5000;
|
||||
|
||||
/** Minimum time before considering an invalid unconfirmed transaction as "stale" */
|
||||
public static final long INVALID_TRANSACTION_STALE_TIMEOUT = 30 * 60 * 1000L; // ms
|
||||
/** Minimum frequency to re-request stale unconfirmed transactions from peers, to recheck validity */
|
||||
public static final long INVALID_TRANSACTION_RECHECK_INTERVAL = 60 * 60 * 1000L; // ms\
|
||||
/** Minimum frequency to re-request expired unconfirmed transactions from peers, to recheck validity
|
||||
* This mainly exists to stop expired transactions from bloating the list */
|
||||
public static final long EXPIRED_TRANSACTION_RECHECK_INTERVAL = 10 * 60 * 1000L; // ms
|
||||
|
||||
|
||||
/** Map of incoming transaction that are in the import queue. Key is transaction data, value is whether signature has been validated. */
|
||||
private final Map<TransactionData, Boolean> incomingTransactions = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
/** Map of recent invalid unconfirmed transactions. Key is base58 transaction signature, value is do-not-request expiry timestamp. */
|
||||
private final Map<String, Long> invalidUnconfirmedTransactions = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
|
||||
public static synchronized TransactionImporter getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new TransactionImporter();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (!Controller.isStopping()) {
|
||||
Thread.sleep(1000L);
|
||||
|
||||
// Process incoming transactions queue
|
||||
processIncomingTransactionsQueue();
|
||||
// Clean up invalid incoming transactions list
|
||||
cleanupInvalidTransactionsList(NTP.getTime());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall through to exit thread
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
isStopping = true;
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
|
||||
// Incoming transactions queue
|
||||
|
||||
private boolean incomingTransactionQueueContains(byte[] signature) {
|
||||
synchronized (incomingTransactions) {
|
||||
return incomingTransactions.keySet().stream().anyMatch(t -> Arrays.equals(t.getSignature(), signature));
|
||||
}
|
||||
}
|
||||
|
||||
private void removeIncomingTransaction(byte[] signature) {
|
||||
incomingTransactions.keySet().removeIf(t -> Arrays.equals(t.getSignature(), signature));
|
||||
}
|
||||
|
||||
private void processIncomingTransactionsQueue() {
|
||||
if (this.incomingTransactions.isEmpty()) {
|
||||
// Nothing to do?
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Take a snapshot of incomingTransactions, so we don't need to lock it while processing
|
||||
Map<TransactionData, Boolean> incomingTransactionsCopy = Map.copyOf(this.incomingTransactions);
|
||||
|
||||
int unvalidatedCount = Collections.frequency(incomingTransactionsCopy.values(), Boolean.FALSE);
|
||||
int validatedCount = 0;
|
||||
|
||||
if (unvalidatedCount > 0) {
|
||||
LOGGER.debug("Validating signatures in incoming transactions queue (size {})...", unvalidatedCount);
|
||||
}
|
||||
|
||||
List<Transaction> sigValidTransactions = new ArrayList<>();
|
||||
|
||||
// Signature validation round - does not require blockchain lock
|
||||
for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) {
|
||||
// Quick exit?
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionData transactionData = transactionEntry.getKey();
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
// 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());
|
||||
|
||||
LOGGER.trace("Ignoring {} transaction {} with invalid signature", transactionData.getType().name(), signature58);
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
|
||||
// Also add to invalidIncomingTransactions map
|
||||
Long now = NTP.getTime();
|
||||
if (now != null) {
|
||||
Long expiry = now + INVALID_TRANSACTION_RECHECK_INTERVAL;
|
||||
LOGGER.trace("Adding stale invalid transaction {} to invalidUnconfirmedTransactions...", signature58);
|
||||
// Add to invalidUnconfirmedTransactions so that we don't keep requesting it
|
||||
invalidUnconfirmedTransactions.put(signature58, expiry);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
// 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);
|
||||
} else {
|
||||
LOGGER.trace(() -> String.format("Transaction %s known to have valid signature", Base58.encode(transactionData.getSignature())));
|
||||
}
|
||||
|
||||
// Signature valid - add to shortlist
|
||||
sigValidTransactions.add(transaction);
|
||||
}
|
||||
|
||||
if (unvalidatedCount > 0) {
|
||||
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 (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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
LOGGER.debug("Processing incoming transactions queue (size {})...", sigValidTransactions.size());
|
||||
|
||||
// Import transactions with valid signatures
|
||||
try {
|
||||
for (int i = 0; i < sigValidTransactions.size(); ++i) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Synchronizer.getInstance().isSyncRequestPending()) {
|
||||
LOGGER.debug("Breaking out of transaction processing with {} remaining, because a sync request is pending", sigValidTransactions.size() - i);
|
||||
return;
|
||||
}
|
||||
|
||||
Transaction transaction = sigValidTransactions.get(i);
|
||||
TransactionData transactionData = transaction.getTransactionData();
|
||||
|
||||
Transaction.ValidationResult validationResult = transaction.importAsUnconfirmed();
|
||||
|
||||
switch (validationResult) {
|
||||
case TRANSACTION_ALREADY_EXISTS: {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
break;
|
||||
}
|
||||
|
||||
case NO_BLOCKCHAIN_LOCK: {
|
||||
// Is this even possible considering we acquired blockchain lock above?
|
||||
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
break;
|
||||
}
|
||||
|
||||
case OK: {
|
||||
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
|
||||
break;
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
Long now = NTP.getTime();
|
||||
if (now != null && now - transactionData.getTimestamp() > INVALID_TRANSACTION_STALE_TIMEOUT) {
|
||||
Long expiryLength = INVALID_TRANSACTION_RECHECK_INTERVAL;
|
||||
|
||||
if (validationResult == Transaction.ValidationResult.TIMESTAMP_TOO_OLD) {
|
||||
// Use shorter recheck interval for expired transactions
|
||||
expiryLength = EXPIRED_TRANSACTION_RECHECK_INTERVAL;
|
||||
}
|
||||
|
||||
Long expiry = now + expiryLength;
|
||||
LOGGER.trace("Adding stale invalid transaction {} to invalidUnconfirmedTransactions...", signature58);
|
||||
// Invalid, unconfirmed transaction has become stale - add to invalidUnconfirmedTransactions so that we don't keep requesting it
|
||||
invalidUnconfirmedTransactions.put(signature58, expiry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction has been processed, even if only to reject it
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
}
|
||||
} finally {
|
||||
LOGGER.debug("Finished processing incoming transactions queue");
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue while processing incoming transactions", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupInvalidTransactionsList(Long now) {
|
||||
if (now == null) {
|
||||
return;
|
||||
}
|
||||
// Periodically remove invalid unconfirmed transactions from the list, so that they can be fetched again
|
||||
invalidUnconfirmedTransactions.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() < now);
|
||||
}
|
||||
|
||||
|
||||
// Network handlers
|
||||
|
||||
public void onNetworkTransactionMessage(Peer peer, Message message) {
|
||||
TransactionMessage transactionMessage = (TransactionMessage) message;
|
||||
TransactionData transactionData = transactionMessage.getTransactionData();
|
||||
|
||||
if (this.incomingTransactions.size() < MAX_INCOMING_TRANSACTIONS) {
|
||||
synchronized (this.incomingTransactions) {
|
||||
if (!incomingTransactionQueueContains(transactionData.getSignature())) {
|
||||
this.incomingTransactions.put(transactionData, Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onNetworkGetTransactionMessage(Peer peer, Message message) {
|
||||
GetTransactionMessage getTransactionMessage = (GetTransactionMessage) message;
|
||||
byte[] signature = getTransactionMessage.getSignature();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData == null) {
|
||||
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;
|
||||
}
|
||||
|
||||
Message transactionMessage = new TransactionMessage(transactionData);
|
||||
transactionMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(transactionMessage))
|
||||
peer.disconnect("failed to send transaction");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
|
||||
} catch (TransformationException e) {
|
||||
LOGGER.error(String.format("Serialization issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void onNetworkGetUnconfirmedTransactionsMessage(Peer peer, Message message) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = Collections.emptyList();
|
||||
|
||||
// If we're NOT up-to-date then don't send out unconfirmed transactions
|
||||
// as it's possible they are already included in a later block that we don't have.
|
||||
if (Controller.getInstance().isUpToDate())
|
||||
signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
|
||||
|
||||
Message transactionSignaturesMessage = new TransactionSignaturesMessage(signatures);
|
||||
if (!peer.sendMessage(transactionSignaturesMessage))
|
||||
peer.disconnect("failed to send unconfirmed transaction signatures");
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while sending unconfirmed transaction signatures to peer %s", peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void onNetworkTransactionSignaturesMessage(Peer peer, Message message) {
|
||||
TransactionSignaturesMessage transactionSignaturesMessage = (TransactionSignaturesMessage) message;
|
||||
List<byte[]> signatures = transactionSignaturesMessage.getSignatures();
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (byte[] signature : signatures) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
if (invalidUnconfirmedTransactions.containsKey(signature58)) {
|
||||
// Previously invalid transaction - don't keep requesting it
|
||||
// It will be periodically removed from invalidUnconfirmedTransactions to allow for rechecks
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore if this transaction is in the queue
|
||||
if (incomingTransactionQueueContains(signature)) {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing queued transaction %s from peer %s", Base58.encode(signature), peer));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do we have it already? (Before requesting transaction data itself)
|
||||
if (repository.getTransactionRepository().exists(signature)) {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s from peer %s", Base58.encode(signature), peer));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check isInterrupted() here and exit fast
|
||||
if (Thread.currentThread().isInterrupted())
|
||||
return;
|
||||
|
||||
// Fetch actual transaction data from peer
|
||||
Message getTransactionMessage = new GetTransactionMessage(signature);
|
||||
if (!peer.sendMessage(getTransactionMessage)) {
|
||||
peer.disconnect("failed to request transaction");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while processing unconfirmed transactions from peer %s", peer), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,11 +37,16 @@ public class ArbitraryDataBuildManager extends Thread {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Arbitrary Data Build Manager");
|
||||
|
||||
try {
|
||||
// Use a fixed thread pool to execute the arbitrary data build actions (currently just a single thread)
|
||||
// This can be expanded to have multiple threads processing the build queue when needed
|
||||
ExecutorService arbitraryDataBuildExecutor = Executors.newFixedThreadPool(1);
|
||||
arbitraryDataBuildExecutor.execute(new ArbitraryDataBuilderThread());
|
||||
int threadCount = 5;
|
||||
ExecutorService arbitraryDataBuildExecutor = Executors.newFixedThreadPool(threadCount);
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
arbitraryDataBuildExecutor.execute(new ArbitraryDataBuilderThread());
|
||||
}
|
||||
|
||||
while (!isStopping) {
|
||||
// Nothing to do yet
|
||||
@@ -101,7 +106,7 @@ public class ArbitraryDataBuildManager extends Thread {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGGER.info("Added {} to build queue", queueItem);
|
||||
log(queueItem, String.format("Added %s to build queue", queueItem));
|
||||
|
||||
// Added to queue
|
||||
return true;
|
||||
@@ -149,7 +154,7 @@ public class ArbitraryDataBuildManager extends Thread {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGGER.info("Added {} to failed builds list", queueItem);
|
||||
log(queueItem, String.format("Added %s to failed builds list", queueItem));
|
||||
|
||||
// Added to queue
|
||||
return true;
|
||||
@@ -182,4 +187,17 @@ public class ArbitraryDataBuildManager extends Thread {
|
||||
public boolean getBuildInProgress() {
|
||||
return this.buildInProgress;
|
||||
}
|
||||
|
||||
private void log(ArbitraryDataBuildQueueItem queueItem, String message) {
|
||||
if (queueItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (queueItem.isHighPriority()) {
|
||||
LOGGER.info(message);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -20,13 +21,14 @@ public class ArbitraryDataBuilderThread implements Runnable {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Arbitrary Data Build Manager");
|
||||
Thread.currentThread().setName("Arbitrary Data Builder Thread");
|
||||
ArbitraryDataBuildManager buildManager = ArbitraryDataBuildManager.getInstance();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
Thread.sleep(100);
|
||||
|
||||
if (buildManager.arbitraryDataBuildQueue == null) {
|
||||
continue;
|
||||
@@ -35,48 +37,57 @@ public class ArbitraryDataBuilderThread implements Runnable {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find resources that are queued for building
|
||||
Map.Entry<String, ArbitraryDataBuildQueueItem> next = buildManager.arbitraryDataBuildQueue
|
||||
.entrySet().stream()
|
||||
.filter(e -> e.getValue().isQueued())
|
||||
.findFirst().get();
|
||||
|
||||
if (next == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ArbitraryDataBuildQueueItem queueItem = next.getValue();
|
||||
ArbitraryDataBuildQueueItem queueItem = null;
|
||||
|
||||
if (queueItem == null) {
|
||||
this.removeFromQueue(queueItem);
|
||||
// Find resources that are queued for building (sorted by highest priority first)
|
||||
synchronized (buildManager.arbitraryDataBuildQueue) {
|
||||
Map.Entry<String, ArbitraryDataBuildQueueItem> next = buildManager.arbitraryDataBuildQueue
|
||||
.entrySet().stream()
|
||||
.filter(e -> e.getValue().isQueued())
|
||||
.sorted(Comparator.comparing(item -> item.getValue().getPriority()))
|
||||
.reduce((first, second) -> second).orElse(null);
|
||||
|
||||
if (next == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
queueItem = next.getValue();
|
||||
|
||||
if (queueItem == null) {
|
||||
this.removeFromQueue(queueItem);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore builds that have failed recently
|
||||
if (buildManager.isInFailedBuildsList(queueItem)) {
|
||||
this.removeFromQueue(queueItem);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the start timestamp, to prevent other threads from building it at the same time
|
||||
queueItem.prepareForBuild();
|
||||
}
|
||||
|
||||
// Ignore builds that have failed recently
|
||||
if (buildManager.isInFailedBuildsList(queueItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Perform the build
|
||||
LOGGER.info("Building {}...", queueItem);
|
||||
log(queueItem, String.format("Building %s... priority: %d", queueItem, queueItem.getPriority()));
|
||||
queueItem.build();
|
||||
this.removeFromQueue(queueItem);
|
||||
LOGGER.info("Finished building {}", queueItem);
|
||||
log(queueItem, String.format("Finished building %s", queueItem));
|
||||
|
||||
} catch (MissingDataException e) {
|
||||
LOGGER.info("Missing data for {}: {}", queueItem, e.getMessage());
|
||||
log(queueItem, String.format("Missing data for %s: %s", queueItem, e.getMessage()));
|
||||
queueItem.setFailed(true);
|
||||
this.removeFromQueue(queueItem);
|
||||
// Don't add to the failed builds list, as we may want to retry sooner
|
||||
|
||||
} catch (IOException | DataException | RuntimeException e) {
|
||||
LOGGER.info("Error building {}: {}", queueItem, e.getMessage());
|
||||
log(queueItem, String.format("Error building %s: %s", queueItem, e.getMessage()));
|
||||
// Something went wrong - so remove it from the queue, and add to failed builds list
|
||||
queueItem.setFailed(true);
|
||||
buildManager.addToFailedBuildsList(queueItem);
|
||||
@@ -95,4 +106,17 @@ public class ArbitraryDataBuilderThread implements Runnable {
|
||||
}
|
||||
ArbitraryDataBuildManager.getInstance().arbitraryDataBuildQueue.remove(queueItem.getUniqueKey());
|
||||
}
|
||||
|
||||
private void log(ArbitraryDataBuildQueueItem queueItem, String message) {
|
||||
if (queueItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (queueItem.isHighPriority()) {
|
||||
LOGGER.info(message);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,10 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
|
||||
// LOGGER.info("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (signatures == null || signatures.isEmpty()) {
|
||||
offset = 0;
|
||||
continue;
|
||||
@@ -117,6 +121,10 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
// Loop through the signatures in this batch
|
||||
for (int i=0; i<signatures.size(); i++) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] signature = signatures.get(i);
|
||||
if (signature == null) {
|
||||
continue;
|
||||
@@ -172,9 +180,6 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
arbitraryTransactionData.getName(), Base58.encode(signature)));
|
||||
|
||||
ArbitraryTransactionUtils.deleteCompleteFileAndChunks(arbitraryTransactionData);
|
||||
|
||||
// We should also remove peers for this transaction from the lookup table to save space
|
||||
this.removePeersHostingTransactionData(repository, arbitraryTransactionData);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -214,7 +219,11 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Check if there are any hosted files that don't have matching transactions
|
||||
this.checkForExpiredTransactions(repository);
|
||||
// UPDATE: This has been disabled for now as it was deleting valid transactions
|
||||
// and causing chunks to go missing on the network. If ever re-enabled, we MUST
|
||||
// ensure that original copies of data aren't deleted, and that sufficient time
|
||||
// is allowed (ideally several hours) before treating a transaction as missing.
|
||||
// this.checkForExpiredTransactions(repository);
|
||||
|
||||
// Delete additional data at random if we're over our storage limit
|
||||
// Use the DELETION_THRESHOLD so that we only start deleting once the hard limit is reached
|
||||
@@ -231,6 +240,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
// Delete random data associated with name if we're over our storage limit for this name
|
||||
// Use the DELETION_THRESHOLD, for the same reasons as above
|
||||
for (String followedName : storageManager.followedNames()) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
if (!storageManager.isStorageSpaceAvailableForName(repository, followedName, DELETION_THRESHOLD)) {
|
||||
this.storageLimitReachedForName(repository, followedName);
|
||||
}
|
||||
@@ -253,6 +265,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
// Loop through each path and find those without matching signatures
|
||||
for (Path path : allPaths) {
|
||||
if (isStopping) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
String[] contents = path.toFile().list();
|
||||
if (contents == null || contents.length == 0) {
|
||||
@@ -279,6 +294,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
private void checkForExpiredTransactions(Repository repository) {
|
||||
List<Path> expiredPaths = this.findPathsWithNoAssociatedTransaction(repository);
|
||||
for (Path expiredPath : expiredPaths) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
LOGGER.info("Found path with no associated transaction: {}", expiredPath.toString());
|
||||
this.safeDeleteDirectory(expiredPath.toFile(), "no matching transaction");
|
||||
}
|
||||
@@ -300,6 +318,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
// when they reach their storage limit
|
||||
Path dataPath = Paths.get(Settings.getInstance().getDataPath());
|
||||
for (int i=0; i<CHUNK_DELETION_BATCH_SIZE; i++) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
this.deleteRandomFile(repository, dataPath.toFile(), null);
|
||||
}
|
||||
|
||||
@@ -318,6 +339,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
// when they reach their storage limit
|
||||
Path dataPath = Paths.get(Settings.getInstance().getDataPath());
|
||||
for (int i=0; i<CHUNK_DELETION_BATCH_SIZE; i++) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
this.deleteRandomFile(repository, dataPath.toFile(), name);
|
||||
}
|
||||
}
|
||||
@@ -410,16 +434,6 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void removePeersHostingTransactionData(Repository repository, ArbitraryTransactionData transactionData) {
|
||||
byte[] signature = transactionData.getSignature();
|
||||
try {
|
||||
repository.getArbitraryRepository().deleteArbitraryPeersWithSignature(signature);
|
||||
repository.saveChanges();
|
||||
} catch (DataException e) {
|
||||
LOGGER.debug("Unable to delete peers from lookup table for signature: {}", Base58.encode(signature));
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupTempDirectory(String folder, long now, long minAge) {
|
||||
String baseDir = Settings.getInstance().getTempDataPath();
|
||||
Path tempDir = Paths.get(baseDir, folder);
|
||||
@@ -429,6 +443,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
final File[] directories = tempDir.toFile().listFiles();
|
||||
if (directories != null) {
|
||||
for (final File directory : directories) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
contentsCount++;
|
||||
|
||||
// We're expecting the contents of each subfolder to be a directory
|
||||
@@ -464,6 +481,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
final File[] directories = readerCacheNamesPath.toFile().listFiles();
|
||||
if (directories != null) {
|
||||
for (final File directory : directories) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete data relating to blocked names
|
||||
String name = directory.getName();
|
||||
@@ -489,6 +509,9 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
final File[] directories = readerNameCachePath.toFile().listFiles();
|
||||
if (directories != null) {
|
||||
for (final File directory : directories) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
// Each directory is a "service" type
|
||||
String service = directory.getName();
|
||||
this.cleanupReaderCacheForNameAndService(name, service, now);
|
||||
|
||||
@@ -5,6 +5,9 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFileChunk;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.arbitrary.ArbitraryDirectConnectionInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryRelayInfo;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.Network;
|
||||
@@ -22,12 +25,15 @@ import org.qortal.utils.Triple;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static org.qortal.controller.arbitrary.ArbitraryDataFileManager.MAX_FILE_HASH_RESPONSES;
|
||||
|
||||
public class ArbitraryDataFileListManager {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileListManager.class);
|
||||
|
||||
private static ArbitraryDataFileListManager instance;
|
||||
|
||||
private static String MIN_PEER_VERSION_FOR_FILE_LIST_STATS = "3.2.0";
|
||||
|
||||
/**
|
||||
* Map of recent incoming requests for ARBITRARY transaction data file lists.
|
||||
@@ -57,9 +63,9 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
|
||||
/** Maximum number of seconds that a file list relay request is able to exist on the network */
|
||||
private static long RELAY_REQUEST_MAX_DURATION = 5000L;
|
||||
public static long RELAY_REQUEST_MAX_DURATION = 5000L;
|
||||
/** Maximum number of hops that a file list relay request is allowed to make */
|
||||
private static int RELAY_REQUEST_MAX_HOPS = 3;
|
||||
public static int RELAY_REQUEST_MAX_HOPS = 4;
|
||||
|
||||
|
||||
private ArbitraryDataFileListManager() {
|
||||
@@ -236,9 +242,11 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
|
||||
|
||||
// Lookup file lists by signature
|
||||
// Lookup file lists by signature (and optionally hashes)
|
||||
|
||||
public boolean fetchArbitraryDataFileList(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
byte[] digest = arbitraryTransactionData.getData();
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
|
||||
@@ -260,11 +268,26 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
this.addToSignatureRequests(signature58, true, false);
|
||||
|
||||
List<Peer> handshakedPeers = Network.getInstance().getHandshakedPeers();
|
||||
LOGGER.debug(String.format("Sending data file list request for signature %s to %d peers...", signature58, handshakedPeers.size()));
|
||||
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
List<byte[]> missingHashes = null;
|
||||
|
||||
// Find hashes that we are missing
|
||||
try {
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
missingHashes = arbitraryDataFile.missingHashes();
|
||||
} catch (DataException e) {
|
||||
// Leave missingHashes as null, so that all hashes are requested
|
||||
}
|
||||
int hashCount = missingHashes != null ? missingHashes.size() : 0;
|
||||
|
||||
LOGGER.debug(String.format("Sending data file list request for signature %s with %d hashes to %d peers...", signature58, hashCount, handshakedPeers.size()));
|
||||
|
||||
// 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, now, 0);
|
||||
Message getArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, missingHashes, now, 0, requestingPeer);
|
||||
|
||||
// Save our request into requests map
|
||||
Triple<String, Peer, Long> requestEntry = new Triple<>(signature58, null, NTP.getTime());
|
||||
@@ -304,6 +327,64 @@ public class ArbitraryDataFileListManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean fetchArbitraryDataFileList(Peer peer, byte[] signature) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
|
||||
// Require an NTP sync
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int hashCount = 0;
|
||||
LOGGER.debug(String.format("Sending data file list request for signature %s with %d hashes to peer %s...", signature58, hashCount, peer));
|
||||
|
||||
// Build request
|
||||
// Use a time in the past, so that the recipient peer doesn't try and relay it
|
||||
// Also, set hashes to null since it's easier to request all hashes than it is to determine which ones we need
|
||||
// This could be optimized in the future
|
||||
long timestamp = now - 60000L;
|
||||
List<byte[]> hashes = null;
|
||||
Message getArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, timestamp, 0, null);
|
||||
|
||||
// Save our request into requests map
|
||||
Triple<String, Peer, Long> requestEntry = new Triple<>(signature58, null, NTP.getTime());
|
||||
|
||||
// Assign random ID to this message
|
||||
int id;
|
||||
do {
|
||||
id = new Random().nextInt(Integer.MAX_VALUE - 1) + 1;
|
||||
|
||||
// Put queue into map (keyed by message ID) so we can poll for a response
|
||||
// If putIfAbsent() doesn't return null, then this ID is already taken
|
||||
} while (arbitraryDataFileListRequests.put(id, requestEntry) != null);
|
||||
getArbitraryDataFileListMessage.setId(id);
|
||||
|
||||
// Send the request
|
||||
peer.sendMessage(getArbitraryDataFileListMessage);
|
||||
|
||||
// Poll to see if data has arrived
|
||||
final long singleWait = 100;
|
||||
long totalWait = 0;
|
||||
while (totalWait < ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT) {
|
||||
try {
|
||||
Thread.sleep(singleWait);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
requestEntry = arbitraryDataFileListRequests.get(id);
|
||||
if (requestEntry == null)
|
||||
return false;
|
||||
|
||||
if (requestEntry.getA() == null)
|
||||
break;
|
||||
|
||||
totalWait += singleWait;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void deleteFileListRequestsForSignature(byte[] signature) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
for (Iterator<Map.Entry<Integer, Triple<String, Peer, Long>>> it = arbitraryDataFileListRequests.entrySet().iterator(); it.hasNext();) {
|
||||
@@ -330,6 +411,13 @@ public class ArbitraryDataFileListManager {
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage = (ArbitraryDataFileListMessage) message;
|
||||
LOGGER.debug("Received hash list from peer {} with {} hashes", peer, arbitraryDataFileListMessage.getHashes().size());
|
||||
|
||||
if (LOGGER.isDebugEnabled() && arbitraryDataFileListMessage.getRequestTime() != null) {
|
||||
long totalRequestTime = NTP.getTime() - arbitraryDataFileListMessage.getRequestTime();
|
||||
LOGGER.debug("totalRequestTime: {}, requestHops: {}, peerAddress: {}, isRelayPossible: {}",
|
||||
totalRequestTime, arbitraryDataFileListMessage.getRequestHops(),
|
||||
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
||||
}
|
||||
|
||||
// Do we have a pending request for this data?
|
||||
Triple<String, Peer, Long> request = arbitraryDataFileListRequests.get(message.getId());
|
||||
if (request == null || request.getA() == null) {
|
||||
@@ -350,7 +438,6 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
|
||||
ArbitraryTransactionData arbitraryTransactionData = null;
|
||||
ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance();
|
||||
|
||||
// Check transaction exists and hashes are correct
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -377,8 +464,28 @@ public class ArbitraryDataFileListManager {
|
||||
// }
|
||||
|
||||
if (!isRelayRequest || !Settings.getInstance().isRelayModeEnabled()) {
|
||||
// Go and fetch the actual data, since this isn't a relay request
|
||||
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, hashes);
|
||||
Long now = NTP.getTime();
|
||||
|
||||
if (ArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.size() < MAX_FILE_HASH_RESPONSES) {
|
||||
// Keep track of the hashes this peer reports to have access to
|
||||
for (byte[] hash : hashes) {
|
||||
String hash58 = Base58.encode(hash);
|
||||
|
||||
// Treat null request hops as 100, so that they are able to be sorted (and put to the end of the list)
|
||||
int requestHops = arbitraryDataFileListMessage.getRequestHops() != null ? arbitraryDataFileListMessage.getRequestHops() : 100;
|
||||
|
||||
ArbitraryFileListResponseInfo responseInfo = new ArbitraryFileListResponseInfo(hash58, signature58,
|
||||
peer, now, arbitraryDataFileListMessage.getRequestTime(), requestHops);
|
||||
|
||||
ArbitraryDataFileManager.getInstance().arbitraryDataFileHashResponses.add(responseInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep track of the source peer, for direct connections
|
||||
if (arbitraryDataFileListMessage.getPeerAddress() != null) {
|
||||
ArbitraryDataFileManager.getInstance().addDirectConnectionInfoIfUnique(
|
||||
new ArbitraryDirectConnectionInfo(signature, arbitraryDataFileListMessage.getPeerAddress(), hashes, now));
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
@@ -391,19 +498,36 @@ public class ArbitraryDataFileListManager {
|
||||
if (!isBlocked) {
|
||||
Peer requestingPeer = request.getB();
|
||||
if (requestingPeer != null) {
|
||||
Long requestTime = arbitraryDataFileListMessage.getRequestTime();
|
||||
Integer requestHops = arbitraryDataFileListMessage.getRequestHops();
|
||||
|
||||
// Add each hash to our local mapping so we know who to ask later
|
||||
Long now = NTP.getTime();
|
||||
for (byte[] hash : hashes) {
|
||||
String hash58 = Base58.encode(hash);
|
||||
Triple<String, Peer, Long> value = new Triple<>(signature58, peer, now);
|
||||
if (arbitraryDataFileManager.arbitraryRelayMap.putIfAbsent(hash58, value) == null) {
|
||||
LOGGER.debug("Added {} to relay map: {}, {}, {}", hash58, signature58, peer, now);
|
||||
}
|
||||
ArbitraryRelayInfo relayInfo = new ArbitraryRelayInfo(hash58, signature58, peer, now, requestTime, requestHops);
|
||||
ArbitraryDataFileManager.getInstance().addToRelayMap(relayInfo);
|
||||
}
|
||||
|
||||
// Bump requestHops if it exists
|
||||
if (requestHops != null) {
|
||||
requestHops++;
|
||||
}
|
||||
|
||||
ArbitraryDataFileListMessage forwardArbitraryDataFileListMessage;
|
||||
|
||||
// Remove optional parameters if the requesting peer doesn't support it yet
|
||||
// A message with less statistical data is better than no message at all
|
||||
if (!requestingPeer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
|
||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
} else {
|
||||
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
|
||||
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
|
||||
}
|
||||
|
||||
// Forward to requesting peer
|
||||
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
|
||||
if (!requestingPeer.sendMessage(arbitraryDataFileListMessage)) {
|
||||
if (!requestingPeer.sendMessage(forwardArbitraryDataFileListMessage)) {
|
||||
requestingPeer.disconnect("failed to forward arbitrary data file list");
|
||||
}
|
||||
}
|
||||
@@ -427,15 +551,25 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
// If we've seen this request recently, then ignore
|
||||
if (arbitraryDataFileListRequests.putIfAbsent(message.getId(), newEntry) != null) {
|
||||
LOGGER.debug("Ignoring hash list request from peer {} for signature {}", peer, signature58);
|
||||
LOGGER.trace("Ignoring hash list request from peer {} for signature {}", peer, signature58);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Received hash list request from peer {} for signature {}", peer, signature58);
|
||||
List<byte[]> requestedHashes = getArbitraryDataFileListMessage.getHashes();
|
||||
int hashCount = requestedHashes != null ? requestedHashes.size() : 0;
|
||||
String requestingPeer = getArbitraryDataFileListMessage.getRequestingPeer();
|
||||
|
||||
if (requestingPeer != null) {
|
||||
LOGGER.debug("Received hash list request with {} hashes from peer {} (requesting peer {}) for signature {}", hashCount, peer, requestingPeer, signature58);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Received hash list request with {} hashes from peer {} for signature {}", hashCount, peer, signature58);
|
||||
}
|
||||
|
||||
List<byte[]> hashes = new ArrayList<>();
|
||||
ArbitraryTransactionData transactionData = null;
|
||||
boolean allChunksExist = false;
|
||||
boolean hasMetadata = false;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
@@ -451,36 +585,38 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
// Load file(s) and add any that exist to the list of hashes
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
if (metadataHash != null) {
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
|
||||
// Assume all chunks exists, unless one can't be found below
|
||||
allChunksExist = true;
|
||||
// If the peer didn't supply a hash list, we need to return all hashes for this transaction
|
||||
if (requestedHashes == null || requestedHashes.isEmpty()) {
|
||||
requestedHashes = new ArrayList<>();
|
||||
|
||||
// If we have the metadata file, add its hash
|
||||
if (arbitraryDataFile.getMetadataFile().exists()) {
|
||||
hashes.add(arbitraryDataFile.getMetadataHash());
|
||||
// Add the metadata file
|
||||
if (arbitraryDataFile.getMetadataHash() != null) {
|
||||
requestedHashes.add(arbitraryDataFile.getMetadataHash());
|
||||
hasMetadata = true;
|
||||
}
|
||||
|
||||
// Add the chunk hashes
|
||||
if (arbitraryDataFile.getChunkHashes().size() > 0) {
|
||||
requestedHashes.addAll(arbitraryDataFile.getChunkHashes());
|
||||
}
|
||||
// Add complete file if there are no hashes
|
||||
else {
|
||||
allChunksExist = false;
|
||||
requestedHashes.add(arbitraryDataFile.getHash());
|
||||
}
|
||||
}
|
||||
|
||||
for (ArbitraryDataFileChunk chunk : arbitraryDataFile.getChunks()) {
|
||||
if (chunk.exists()) {
|
||||
hashes.add(chunk.getHash());
|
||||
//LOGGER.trace("Added hash {}", chunk.getHash58());
|
||||
} else {
|
||||
LOGGER.trace("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
|
||||
allChunksExist = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This transaction has no chunks, so include the complete file if we have it
|
||||
if (arbitraryDataFile.exists()) {
|
||||
hashes.add(arbitraryDataFile.getHash());
|
||||
allChunksExist = true;
|
||||
}
|
||||
else {
|
||||
// Assume all chunks exists, unless one can't be found below
|
||||
allChunksExist = true;
|
||||
|
||||
for (byte[] requestedHash : requestedHashes) {
|
||||
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash(requestedHash, signature);
|
||||
if (chunk.exists()) {
|
||||
hashes.add(chunk.getHash());
|
||||
//LOGGER.trace("Added hash {}", chunk.getHash58());
|
||||
} else {
|
||||
LOGGER.trace("Couldn't add hash {} because it doesn't exist", chunk.getHash58());
|
||||
allChunksExist = false;
|
||||
}
|
||||
}
|
||||
@@ -491,9 +627,18 @@ public class ArbitraryDataFileListManager {
|
||||
LOGGER.error(String.format("Repository issue while fetching arbitrary file list for peer %s", peer), e);
|
||||
}
|
||||
|
||||
// If the only file we have is the metadata then we shouldn't respond. Most nodes will already have that,
|
||||
// or can use the separate metadata protocol to fetch it. This should greatly reduce network spam.
|
||||
if (hasMetadata && hashes.size() == 1) {
|
||||
hashes.clear();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -501,8 +646,20 @@ public class ArbitraryDataFileListManager {
|
||||
arbitraryDataFileListRequests.put(message.getId(), newEntry);
|
||||
}
|
||||
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
String ourAddress = Network.getInstance().getOurExternalIpAddressAndPort();
|
||||
ArbitraryDataFileListMessage arbitraryDataFileListMessage;
|
||||
|
||||
// Remove optional parameters if the requesting peer doesn't support it yet
|
||||
// A message with less statistical data is better than no message at all
|
||||
if (!peer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
|
||||
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
|
||||
} else {
|
||||
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature,
|
||||
hashes, NTP.getTime(), 0, ourAddress, true);
|
||||
}
|
||||
|
||||
arbitraryDataFileListMessage.setId(message.getId());
|
||||
|
||||
if (!peer.sendMessage(arbitraryDataFileListMessage)) {
|
||||
LOGGER.debug("Couldn't send list of hashes");
|
||||
peer.disconnect("failed to send list of hashes");
|
||||
@@ -524,8 +681,7 @@ public class ArbitraryDataFileListManager {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
|
||||
int requestHops = getArbitraryDataFileListMessage.getRequestHops();
|
||||
getArbitraryDataFileListMessage.setRequestHops(++requestHops);
|
||||
int requestHops = getArbitraryDataFileListMessage.getRequestHops() + 1;
|
||||
long totalRequestTime = now - requestTime;
|
||||
|
||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||
@@ -533,11 +689,13 @@ public class ArbitraryDataFileListManager {
|
||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||
|
||||
Message relayGetArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops, requestingPeer);
|
||||
|
||||
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 : getArbitraryDataFileListMessage);
|
||||
? null : relayGetArbitraryDataFileListMessage);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.network.ArbitraryPeerData;
|
||||
import org.qortal.data.arbitrary.ArbitraryDirectConnectionInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryRelayInfo;
|
||||
import org.qortal.data.network.PeerData;
|
||||
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.network.message.*;
|
||||
@@ -18,29 +20,50 @@ import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ArbitraryDataFileManager {
|
||||
public class ArbitraryDataFileManager extends Thread {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileManager.class);
|
||||
|
||||
private static ArbitraryDataFileManager instance;
|
||||
private volatile boolean isStopping = false;
|
||||
|
||||
|
||||
/**
|
||||
* Map to keep track of our in progress (outgoing) arbitrary data file requests
|
||||
*/
|
||||
private Map<String, Long> arbitraryDataFileRequests = Collections.synchronizedMap(new HashMap<>());
|
||||
public Map<String, Long> arbitraryDataFileRequests = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
/**
|
||||
* Map to keep track of hashes that we might need to relay, keyed by the hash of the file (base58 encoded).
|
||||
* Value is comprised of the base58-encoded signature, the peer that is hosting it, and the timestamp that it was added
|
||||
* Map to keep track of hashes that we might need to relay
|
||||
*/
|
||||
public Map<String, Triple<String, Peer, Long>> arbitraryRelayMap = Collections.synchronizedMap(new HashMap<>());
|
||||
public List<ArbitraryRelayInfo> arbitraryRelayMap = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* List to keep track of any arbitrary data file hash responses
|
||||
*/
|
||||
public final List<ArbitraryFileListResponseInfo> arbitraryDataFileHashResponses = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* List to keep track of peers potentially available for direct connections, based on recent requests
|
||||
*/
|
||||
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;
|
||||
|
||||
|
||||
private ArbitraryDataFileManager() {
|
||||
@@ -53,6 +76,32 @@ public class ArbitraryDataFileManager {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Arbitrary Data File Manager");
|
||||
|
||||
try {
|
||||
// Use a fixed thread pool to execute the arbitrary data file requests
|
||||
int threadCount = 10;
|
||||
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
||||
}
|
||||
|
||||
while (!isStopping) {
|
||||
// Nothing to do yet
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall-through to exit thread...
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
isStopping = true;
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
|
||||
public void cleanupRequestCache(Long now) {
|
||||
if (now == null) {
|
||||
@@ -62,29 +111,20 @@ public class ArbitraryDataFileManager {
|
||||
arbitraryDataFileRequests.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() < requestMinimumTimestamp);
|
||||
|
||||
final long relayMinimumTimestamp = now - ArbitraryDataManager.getInstance().ARBITRARY_RELAY_TIMEOUT;
|
||||
arbitraryRelayMap.entrySet().removeIf(entry -> entry.getValue().getC() == null || entry.getValue().getC() < relayMinimumTimestamp);
|
||||
arbitraryRelayMap.removeIf(entry -> entry == null || entry.getTimestamp() == null || entry.getTimestamp() < relayMinimumTimestamp);
|
||||
arbitraryDataFileHashResponses.removeIf(entry -> entry.getTimestamp() < relayMinimumTimestamp);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Fetch data files by hash
|
||||
|
||||
public boolean fetchAllArbitraryDataFiles(Repository repository, Peer peer, byte[] signature) {
|
||||
try {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (!(transactionData instanceof ArbitraryTransactionData))
|
||||
return false;
|
||||
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
|
||||
// We use null to represent all hashes associated with this transaction
|
||||
return this.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, null);
|
||||
|
||||
} catch (DataException e) {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean fetchArbitraryDataFiles(Repository repository,
|
||||
Peer peer,
|
||||
byte[] signature,
|
||||
@@ -95,73 +135,58 @@ public class ArbitraryDataFileManager {
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(arbitraryTransactionData.getData(), signature);
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
|
||||
// If hashes are null, we will treat this to mean all data hashes associated with this file
|
||||
if (hashes == null) {
|
||||
if (metadataHash == null) {
|
||||
// This transaction has no metadata/chunks, so use the main file hash
|
||||
hashes = Arrays.asList(arbitraryDataFile.getHash());
|
||||
}
|
||||
else if (!arbitraryDataFile.getMetadataFile().exists()) {
|
||||
// We don't have the metadata file yet, so request it
|
||||
hashes = Arrays.asList(arbitraryDataFile.getMetadataFile().getHash());
|
||||
}
|
||||
else {
|
||||
// Add the chunk hashes
|
||||
hashes = arbitraryDataFile.getChunkHashes();
|
||||
}
|
||||
}
|
||||
|
||||
boolean receivedAtLeastOneFile = false;
|
||||
|
||||
// Now fetch actual data from this peer
|
||||
for (byte[] hash : hashes) {
|
||||
if (isStopping) {
|
||||
return false;
|
||||
}
|
||||
String hash58 = Base58.encode(hash);
|
||||
if (!arbitraryDataFile.chunkExists(hash)) {
|
||||
// Only request the file if we aren't already requesting it from someone else
|
||||
if (!arbitraryDataFileRequests.containsKey(Base58.encode(hash))) {
|
||||
LOGGER.debug("Requesting data file {} from peer {}", hash58, peer);
|
||||
Long startTime = NTP.getTime();
|
||||
ArbitraryDataFileMessage receivedArbitraryDataFileMessage = fetchArbitraryDataFile(peer, null, signature, hash, null);
|
||||
if (receivedArbitraryDataFileMessage != null) {
|
||||
LOGGER.debug("Received data file {} from peer {}", receivedArbitraryDataFileMessage.getArbitraryDataFile().getHash58(), peer);
|
||||
Long endTime = NTP.getTime();
|
||||
if (receivedArbitraryDataFileMessage != null && receivedArbitraryDataFileMessage.getArbitraryDataFile() != null) {
|
||||
LOGGER.debug("Received data file {} from peer {}. Time taken: {} ms", receivedArbitraryDataFileMessage.getArbitraryDataFile().getHash58(), peer, (endTime-startTime));
|
||||
receivedAtLeastOneFile = true;
|
||||
|
||||
// Remove this hash from arbitraryDataFileHashResponses now that we have received it
|
||||
arbitraryDataFileHashResponses.remove(hash58);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Peer {} didn't respond with data file {} for signature {}", peer, Base58.encode(hash), Base58.encode(signature));
|
||||
LOGGER.debug("Peer {} didn't respond with data file {} for signature {}. Time taken: {} ms", peer, Base58.encode(hash), Base58.encode(signature), (endTime-startTime));
|
||||
|
||||
// Remove this hash from arbitraryDataFileHashResponses now that we have failed to receive it
|
||||
arbitraryDataFileHashResponses.remove(hash58);
|
||||
|
||||
// Stop asking for files from this peer
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.trace("Already requesting data file {} for signature {}", arbitraryDataFile, Base58.encode(signature));
|
||||
LOGGER.trace("Already requesting data file {} for signature {} from peer {}", arbitraryDataFile, Base58.encode(signature), peer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Remove this hash from arbitraryDataFileHashResponses because we have a local copy
|
||||
arbitraryDataFileHashResponses.remove(hash58);
|
||||
}
|
||||
}
|
||||
|
||||
if (receivedAtLeastOneFile) {
|
||||
// Update our lookup table to indicate that this peer holds data for this signature
|
||||
String peerAddress = peer.getPeerData().getAddress().toString();
|
||||
ArbitraryPeerData arbitraryPeerData = new ArbitraryPeerData(signature, peer);
|
||||
repository.discardChanges();
|
||||
if (arbitraryPeerData.isPeerAddressValid()) {
|
||||
LOGGER.debug("Adding arbitrary peer: {} for signature {}", peerAddress, Base58.encode(signature));
|
||||
repository.getArbitraryRepository().save(arbitraryPeerData);
|
||||
repository.saveChanges();
|
||||
}
|
||||
|
||||
// Invalidate the hosted transactions cache as we are now hosting something new
|
||||
ArbitraryDataStorageManager.getInstance().invalidateHostedTransactionsCache();
|
||||
}
|
||||
|
||||
// Check if we have all the files we need for this transaction
|
||||
if (arbitraryDataFile.allFilesExist()) {
|
||||
// Check if we have all the files we need for this transaction
|
||||
if (arbitraryDataFile.allFilesExist()) {
|
||||
|
||||
// We have all the chunks for this transaction, so we should invalidate the transaction's name's
|
||||
// data cache so that it is rebuilt the next time we serve it
|
||||
ArbitraryDataManager.getInstance().invalidateCache(arbitraryTransactionData);
|
||||
|
||||
// We may also need to broadcast to the network that we are now hosting files for this transaction,
|
||||
// but only if these files are in accordance with our storage policy
|
||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(arbitraryTransactionData)) {
|
||||
// Use a null peer address to indicate our own
|
||||
Message newArbitrarySignatureMessage = new ArbitrarySignaturesMessage(null, 0, Arrays.asList(signature));
|
||||
Network.getInstance().broadcast(broadcastPeer -> newArbitrarySignatureMessage);
|
||||
// We have all the chunks for this transaction, so we should invalidate the transaction's name's
|
||||
// data cache so that it is rebuilt the next time we serve it
|
||||
ArbitraryDataManager.getInstance().invalidateCache(arbitraryTransactionData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,19 +196,20 @@ public class ArbitraryDataFileManager {
|
||||
private ArbitraryDataFileMessage fetchArbitraryDataFile(Peer peer, Peer requestingPeer, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
||||
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
boolean fileAlreadyExists = existingFile.exists();
|
||||
Message message = null;
|
||||
String hash58 = Base58.encode(hash);
|
||||
ArbitraryDataFileMessage arbitraryDataFileMessage;
|
||||
|
||||
// Fetch the file if it doesn't exist locally
|
||||
if (!fileAlreadyExists) {
|
||||
String hash58 = Base58.encode(hash);
|
||||
LOGGER.debug(String.format("Fetching data file %.8s from peer %s", hash58, peer));
|
||||
arbitraryDataFileRequests.put(hash58, NTP.getTime());
|
||||
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(signature, hash);
|
||||
|
||||
Message response = null;
|
||||
try {
|
||||
message = peer.getResponseWithTimeout(getArbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
|
||||
response = peer.getResponseWithTimeout(getArbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
|
||||
} catch (InterruptedException e) {
|
||||
// Will return below due to null message
|
||||
// Will return below due to null response
|
||||
}
|
||||
arbitraryDataFileRequests.remove(hash58);
|
||||
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
|
||||
@@ -191,14 +217,24 @@ public class ArbitraryDataFileManager {
|
||||
// We may need to remove the file list request, if we have all the files for this transaction
|
||||
this.handleFileListRequests(signature);
|
||||
|
||||
if (message == null || message.getType() != Message.MessageType.ARBITRARY_DATA_FILE) {
|
||||
if (response == null) {
|
||||
LOGGER.debug("Received null response from peer {}", peer);
|
||||
return null;
|
||||
}
|
||||
if (response.getType() != MessageType.ARBITRARY_DATA_FILE) {
|
||||
LOGGER.debug("Received response with invalid type: {} from peer {}", response.getType(), peer);
|
||||
return null;
|
||||
}
|
||||
|
||||
ArbitraryDataFileMessage peersArbitraryDataFileMessage = (ArbitraryDataFileMessage) response;
|
||||
arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, peersArbitraryDataFileMessage.getArbitraryDataFile());
|
||||
} else {
|
||||
LOGGER.debug(String.format("File hash %s already exists, so skipping the request", hash58));
|
||||
arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, existingFile);
|
||||
}
|
||||
ArbitraryDataFileMessage arbitraryDataFileMessage = (ArbitraryDataFileMessage) message;
|
||||
|
||||
// We might want to forward the request to the peer that originally requested it
|
||||
this.handleArbitraryDataFileForwarding(requestingPeer, message, originalMessage);
|
||||
this.handleArbitraryDataFileForwarding(requestingPeer, arbitraryDataFileMessage, originalMessage);
|
||||
|
||||
boolean isRelayRequest = (requestingPeer != null);
|
||||
if (isRelayRequest) {
|
||||
@@ -208,16 +244,7 @@ public class ArbitraryDataFileManager {
|
||||
ArbitraryDataFile dataFile = arbitraryDataFileMessage.getArbitraryDataFile();
|
||||
|
||||
// Keep trying to delete the data until it is deleted, or we reach 10 attempts
|
||||
for (int i=0; i<10; i++) {
|
||||
if (dataFile.delete()) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1000L);
|
||||
} catch (InterruptedException e) {
|
||||
// Fall through to exit method
|
||||
}
|
||||
}
|
||||
dataFile.delete(10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,89 +300,243 @@ public class ArbitraryDataFileManager {
|
||||
|
||||
// Fetch data directly from peers
|
||||
|
||||
private List<ArbitraryDirectConnectionInfo> getDirectConnectionInfoForSignature(byte[] signature) {
|
||||
synchronized (directConnectionInfo) {
|
||||
return directConnectionInfo.stream().filter(i -> Arrays.equals(i.getSignature(), signature)).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an ArbitraryDirectConnectionInfo item, but only if one with this peer-signature combination
|
||||
* doesn't already exist.
|
||||
* @param connectionInfo - the direct connection info to add
|
||||
*/
|
||||
public void addDirectConnectionInfoIfUnique(ArbitraryDirectConnectionInfo connectionInfo) {
|
||||
boolean peerAlreadyExists;
|
||||
synchronized (directConnectionInfo) {
|
||||
peerAlreadyExists = directConnectionInfo.stream()
|
||||
.anyMatch(i -> Arrays.equals(i.getSignature(), connectionInfo.getSignature())
|
||||
&& Objects.equals(i.getPeerAddress(), connectionInfo.getPeerAddress()));
|
||||
}
|
||||
if (!peerAlreadyExists) {
|
||||
directConnectionInfo.add(connectionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeDirectConnectionInfo(ArbitraryDirectConnectionInfo connectionInfo) {
|
||||
this.directConnectionInfo.remove(connectionInfo);
|
||||
}
|
||||
|
||||
public boolean fetchDataFilesFromPeersForSignature(byte[] signature) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
ArbitraryDataFileListManager.getInstance().addToSignatureRequests(signature58, false, true);
|
||||
|
||||
// Firstly fetch peers that claim to be hosting files for this signature
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
boolean success = false;
|
||||
|
||||
List<ArbitraryPeerData> peers = repository.getArbitraryRepository().getArbitraryPeerDataForSignature(signature);
|
||||
if (peers == null || peers.isEmpty()) {
|
||||
LOGGER.debug("No peers found for signature {}", signature58);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.debug("Attempting a direct peer connection for signature {}...", signature58);
|
||||
|
||||
// Peers found, so pick a random one and request data from it
|
||||
int index = new SecureRandom().nextInt(peers.size());
|
||||
ArbitraryPeerData arbitraryPeerData = peers.get(index);
|
||||
String peerAddressString = arbitraryPeerData.getPeerAddress();
|
||||
boolean success = Network.getInstance().requestDataFromPeer(peerAddressString, signature);
|
||||
|
||||
// Parse the peer address to find the host and port
|
||||
String host = null;
|
||||
int port = -1;
|
||||
String[] parts = peerAddressString.split(":");
|
||||
if (parts.length > 1) {
|
||||
host = parts[0];
|
||||
port = Integer.parseInt(parts[1]);
|
||||
}
|
||||
|
||||
// If unsuccessful, and using a non-standard port, try a second connection with the default listen port,
|
||||
// since almost all nodes use that. This is a workaround to account for any ephemeral ports that may
|
||||
// have made it into the dataset.
|
||||
if (!success) {
|
||||
if (host != null && port > 0) {
|
||||
int defaultPort = Settings.getInstance().getDefaultListenPort();
|
||||
if (port != defaultPort) {
|
||||
String newPeerAddressString = String.format("%s:%d", host, defaultPort);
|
||||
success = Network.getInstance().requestDataFromPeer(newPeerAddressString, signature);
|
||||
}
|
||||
try {
|
||||
while (!success) {
|
||||
if (isStopping) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Thread.sleep(500L);
|
||||
|
||||
// If _still_ unsuccessful, try matching the peer's IP address with some known peers, and then connect
|
||||
// to each of those in turn until one succeeds.
|
||||
if (!success) {
|
||||
if (host != null) {
|
||||
final String finalHost = host;
|
||||
List<PeerData> knownPeers = Network.getInstance().getAllKnownPeers().stream()
|
||||
.filter(knownPeerData -> knownPeerData.getAddress().getHost().equals(finalHost))
|
||||
.collect(Collectors.toList());
|
||||
// Loop through each match and attempt a connection
|
||||
for (PeerData matchingPeer : knownPeers) {
|
||||
String matchingPeerAddress = matchingPeer.getAddress().toString();
|
||||
success = Network.getInstance().requestDataFromPeer(matchingPeerAddress, signature);
|
||||
if (success) {
|
||||
// Successfully connected, so stop making connections
|
||||
break;
|
||||
// Firstly fetch peers that claim to be hosting files for this signature
|
||||
List<ArbitraryDirectConnectionInfo> connectionInfoList = getDirectConnectionInfoForSignature(signature);
|
||||
if (connectionInfoList == null || connectionInfoList.isEmpty()) {
|
||||
LOGGER.debug("No remaining direct connection peers found for signature {}", signature58);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.debug("Attempting a direct peer connection for signature {}...", signature58);
|
||||
|
||||
// Peers found, so pick one with the highest number of chunks
|
||||
Comparator<ArbitraryDirectConnectionInfo> highestChunkCountFirstComparator =
|
||||
Comparator.comparingInt(ArbitraryDirectConnectionInfo::getHashCount).reversed();
|
||||
ArbitraryDirectConnectionInfo directConnectionInfo = connectionInfoList.stream()
|
||||
.sorted(highestChunkCountFirstComparator).findFirst().orElse(null);
|
||||
|
||||
if (directConnectionInfo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove from the list so that a different peer is tried next time
|
||||
removeDirectConnectionInfo(directConnectionInfo);
|
||||
|
||||
String peerAddressString = directConnectionInfo.getPeerAddress();
|
||||
|
||||
// Parse the peer address to find the host and port
|
||||
String host = null;
|
||||
int port = -1;
|
||||
String[] parts = peerAddressString.split(":");
|
||||
if (parts.length > 1) {
|
||||
host = parts[0];
|
||||
port = Integer.parseInt(parts[1]);
|
||||
} else {
|
||||
// Assume no port included
|
||||
host = peerAddressString;
|
||||
// Use default listen port
|
||||
port = Settings.getInstance().getDefaultListenPort();
|
||||
}
|
||||
|
||||
String peerAddressStringWithPort = String.format("%s:%d", host, port);
|
||||
success = Network.getInstance().requestDataFromPeer(peerAddressStringWithPort, signature);
|
||||
|
||||
int defaultPort = Settings.getInstance().getDefaultListenPort();
|
||||
|
||||
// If unsuccessful, and using a non-standard port, try a second connection with the default listen port,
|
||||
// since almost all nodes use that. This is a workaround to account for any ephemeral ports that may
|
||||
// have made it into the dataset.
|
||||
if (!success) {
|
||||
if (host != null && port > 0) {
|
||||
if (port != defaultPort) {
|
||||
String newPeerAddressString = String.format("%s:%d", host, defaultPort);
|
||||
success = Network.getInstance().requestDataFromPeer(newPeerAddressString, signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep track of the success or failure
|
||||
arbitraryPeerData.markAsAttempted();
|
||||
if (success) {
|
||||
arbitraryPeerData.markAsRetrieved();
|
||||
arbitraryPeerData.incrementSuccesses();
|
||||
}
|
||||
else {
|
||||
arbitraryPeerData.incrementFailures();
|
||||
}
|
||||
repository.discardChanges();
|
||||
repository.getArbitraryRepository().save(arbitraryPeerData);
|
||||
repository.saveChanges();
|
||||
// If _still_ unsuccessful, try matching the peer's IP address with some known peers, and then connect
|
||||
// to each of those in turn until one succeeds.
|
||||
if (!success) {
|
||||
if (host != null) {
|
||||
final String finalHost = host;
|
||||
List<PeerData> knownPeers = Network.getInstance().getAllKnownPeers().stream()
|
||||
.filter(knownPeerData -> knownPeerData.getAddress().getHost().equals(finalHost))
|
||||
.collect(Collectors.toList());
|
||||
// Loop through each match and attempt a connection
|
||||
for (PeerData matchingPeer : knownPeers) {
|
||||
String matchingPeerAddress = matchingPeer.getAddress().toString();
|
||||
int matchingPeerPort = matchingPeer.getAddress().getPort();
|
||||
// Make sure that it's not a port we've already tried
|
||||
if (matchingPeerPort != port && matchingPeerPort != defaultPort) {
|
||||
success = Network.getInstance().requestDataFromPeer(matchingPeerAddress, signature);
|
||||
if (success) {
|
||||
// Successfully connected, so stop making connections
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
if (success) {
|
||||
// We were able to connect with a peer, so track the request
|
||||
ArbitraryDataFileListManager.getInstance().addToSignatureRequests(signature58, false, true);
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.debug("Unable to fetch peer list from repository");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return false;
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
// Relays
|
||||
|
||||
private List<ArbitraryRelayInfo> getRelayInfoListForHash(String hash58) {
|
||||
synchronized (arbitraryRelayMap) {
|
||||
return arbitraryRelayMap.stream()
|
||||
.filter(relayInfo -> Objects.equals(relayInfo.getHash58(), hash58))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private ArbitraryRelayInfo getOptimalRelayInfoEntryForHash(String hash58) {
|
||||
LOGGER.trace("Fetching relay info for hash: {}", hash58);
|
||||
List<ArbitraryRelayInfo> relayInfoList = this.getRelayInfoListForHash(hash58);
|
||||
if (relayInfoList != null && !relayInfoList.isEmpty()) {
|
||||
|
||||
// Remove any with null requestHops
|
||||
relayInfoList.removeIf(r -> r.getRequestHops() == null);
|
||||
|
||||
// If list is now empty, then just return one at random
|
||||
if (relayInfoList.isEmpty()) {
|
||||
return this.getRandomRelayInfoEntryForHash(hash58);
|
||||
}
|
||||
|
||||
// Sort by number of hops (lowest first)
|
||||
relayInfoList.sort(Comparator.comparingInt(ArbitraryRelayInfo::getRequestHops));
|
||||
|
||||
// FUTURE: secondary sort by requestTime?
|
||||
|
||||
ArbitraryRelayInfo relayInfo = relayInfoList.get(0);
|
||||
|
||||
LOGGER.trace("Returning optimal relay info for hash: {} (requestHops {})", hash58, relayInfo.getRequestHops());
|
||||
return relayInfo;
|
||||
}
|
||||
LOGGER.trace("No relay info exists for hash: {}", hash58);
|
||||
return null;
|
||||
}
|
||||
|
||||
private ArbitraryRelayInfo getRandomRelayInfoEntryForHash(String hash58) {
|
||||
LOGGER.trace("Fetching random relay info for hash: {}", hash58);
|
||||
List<ArbitraryRelayInfo> relayInfoList = this.getRelayInfoListForHash(hash58);
|
||||
if (relayInfoList != null && !relayInfoList.isEmpty()) {
|
||||
|
||||
// Pick random item
|
||||
int index = new SecureRandom().nextInt(relayInfoList.size());
|
||||
LOGGER.trace("Returning random relay info for hash: {} (index {})", hash58, index);
|
||||
return relayInfoList.get(index);
|
||||
}
|
||||
LOGGER.trace("No relay info exists for hash: {}", hash58);
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addToRelayMap(ArbitraryRelayInfo newEntry) {
|
||||
if (newEntry == null || !newEntry.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing entry for this peer if it exists, to renew the timestamp
|
||||
this.removeFromRelayMap(newEntry);
|
||||
|
||||
// Re-add
|
||||
arbitraryRelayMap.add(newEntry);
|
||||
LOGGER.debug("Added entry to relay map: {}", newEntry);
|
||||
}
|
||||
|
||||
private void removeFromRelayMap(ArbitraryRelayInfo entry) {
|
||||
arbitraryRelayMap.removeIf(relayInfo -> relayInfo.equals(entry));
|
||||
}
|
||||
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -377,7 +558,7 @@ public class ArbitraryDataFileManager {
|
||||
|
||||
try {
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
Triple<String, Peer, Long> relayInfo = this.arbitraryRelayMap.get(hash58);
|
||||
ArbitraryRelayInfo relayInfo = this.getOptimalRelayInfoEntryForHash(hash58);
|
||||
|
||||
if (arbitraryDataFile.exists()) {
|
||||
LOGGER.trace("Hash {} exists", hash58);
|
||||
@@ -394,15 +575,12 @@ public class ArbitraryDataFileManager {
|
||||
else if (relayInfo != null) {
|
||||
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
||||
// We need to ask this peer for the file
|
||||
Peer peerToAsk = relayInfo.getB();
|
||||
Peer peerToAsk = relayInfo.getPeer();
|
||||
if (peerToAsk != null) {
|
||||
|
||||
// Forward the message to this peer
|
||||
LOGGER.debug("Asking peer {} for hash {}", peerToAsk, hash58);
|
||||
this.fetchArbitraryDataFile(peerToAsk, peer, signature, hash, message);
|
||||
|
||||
// Remove from the map regardless of outcome, as the relay attempt is now considered complete
|
||||
arbitraryRelayMap.remove(hash58);
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Peer {} not found in relay info", peer);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ArbitraryDataFileRequestThread implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileRequestThread.class);
|
||||
|
||||
public ArbitraryDataFileRequestThread() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Arbitrary Data File Request Thread");
|
||||
|
||||
try {
|
||||
while (!Controller.isStopping()) {
|
||||
Long now = NTP.getTime();
|
||||
this.processFileHashes(now);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall-through to exit thread...
|
||||
}
|
||||
}
|
||||
|
||||
private void processFileHashes(Long now) throws InterruptedException {
|
||||
if (Controller.isStopping()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance();
|
||||
String signature58 = null;
|
||||
String hash58 = null;
|
||||
Peer peer = null;
|
||||
boolean shouldProcess = false;
|
||||
|
||||
synchronized (arbitraryDataFileManager.arbitraryDataFileHashResponses) {
|
||||
if (!arbitraryDataFileManager.arbitraryDataFileHashResponses.isEmpty()) {
|
||||
|
||||
// Sort by lowest number of node hops first
|
||||
Comparator<ArbitraryFileListResponseInfo> lowestHopsFirstComparator =
|
||||
Comparator.comparingInt(ArbitraryFileListResponseInfo::getRequestHops);
|
||||
arbitraryDataFileManager.arbitraryDataFileHashResponses.sort(lowestHopsFirstComparator);
|
||||
|
||||
Iterator iterator = arbitraryDataFileManager.arbitraryDataFileHashResponses.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (Controller.isStopping()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArbitraryFileListResponseInfo responseInfo = (ArbitraryFileListResponseInfo) iterator.next();
|
||||
if (responseInfo == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
hash58 = responseInfo.getHash58();
|
||||
peer = responseInfo.getPeer();
|
||||
signature58 = responseInfo.getSignature58();
|
||||
Long timestamp = responseInfo.getTimestamp();
|
||||
|
||||
if (now - timestamp >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT || signature58 == null || peer == null) {
|
||||
// Ignore - to be deleted
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if already requesting, but don't remove, as we might want to retry later
|
||||
if (arbitraryDataFileManager.arbitraryDataFileRequests.containsKey(hash58)) {
|
||||
// Already requesting - leave this attempt for later
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to process this file
|
||||
shouldProcess = true;
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldProcess) {
|
||||
// Nothing to do
|
||||
Thread.sleep(1000L);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] hash = Base58.decode(hash58);
|
||||
byte[] signature = Base58.decode(signature58);
|
||||
|
||||
// Fetch the transaction data
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
if (arbitraryTransactionData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (signature == null || hash == null || peer == null || arbitraryTransactionData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Fetching file {} from peer {} via request thread...", hash58, peer);
|
||||
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, Arrays.asList(hash));
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.debug("Unable to process file hashes: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.network.ArbitraryPeerData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -38,14 +39,26 @@ public class ArbitraryDataManager extends Thread {
|
||||
private int powDifficulty = 14; // Must not be final, as unit tests need to reduce this value
|
||||
|
||||
/** Request timeout when transferring arbitrary data */
|
||||
public static final long ARBITRARY_REQUEST_TIMEOUT = 10 * 1000L; // ms
|
||||
public static final long ARBITRARY_REQUEST_TIMEOUT = 12 * 1000L; // ms
|
||||
|
||||
/** Maximum time to hold information about an in-progress relay */
|
||||
public static final long ARBITRARY_RELAY_TIMEOUT = 30 * 1000L; // ms
|
||||
public static final long ARBITRARY_RELAY_TIMEOUT = 60 * 1000L; // ms
|
||||
|
||||
/** 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;
|
||||
|
||||
private long lastMetadataFetchTime = 0L;
|
||||
private static long METADATA_FETCH_INTERVAL = 5 * 60 * 1000L;
|
||||
|
||||
private long lastDataFetchTime = 0L;
|
||||
private static long DATA_FETCH_INTERVAL = 1 * 60 * 1000L;
|
||||
|
||||
private static ArbitraryDataManager instance;
|
||||
private final Object peerDataLock = new Object();
|
||||
|
||||
@@ -79,7 +92,13 @@ public class ArbitraryDataManager extends Thread {
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Arbitrary Data Manager");
|
||||
|
||||
// Create data directory in case it doesn't exist yet
|
||||
this.createDataDirectory();
|
||||
|
||||
try {
|
||||
// Wait for node to finish starting up and making connections
|
||||
Thread.sleep(2 * 60 * 1000L);
|
||||
|
||||
while (!isStopping) {
|
||||
Thread.sleep(2000);
|
||||
|
||||
@@ -89,7 +108,13 @@ public class ArbitraryDataManager extends Thread {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Peer> peers = Network.getInstance().getHandshakedPeers();
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -99,6 +124,21 @@ public class ArbitraryDataManager extends Thread {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch metadata
|
||||
if (NTP.getTime() - lastMetadataFetchTime >= METADATA_FETCH_INTERVAL) {
|
||||
this.fetchAllMetadata();
|
||||
lastMetadataFetchTime = NTP.getTime();
|
||||
}
|
||||
|
||||
// Check if we need to fetch any data
|
||||
if (NTP.getTime() - lastDataFetchTime < DATA_FETCH_INTERVAL) {
|
||||
// Nothing to do yet
|
||||
continue;
|
||||
}
|
||||
|
||||
// In case the data directory has been deleted...
|
||||
this.createDataDirectory();
|
||||
|
||||
// Fetch data according to storage policy
|
||||
switch (Settings.getInstance().getStoragePolicy()) {
|
||||
case FOLLOWED:
|
||||
@@ -108,6 +148,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
|
||||
case ALL:
|
||||
this.processAll();
|
||||
break;
|
||||
|
||||
case NONE:
|
||||
case VIEWED:
|
||||
@@ -116,6 +157,8 @@ public class ArbitraryDataManager extends Thread {
|
||||
Thread.sleep(60000);
|
||||
break;
|
||||
}
|
||||
|
||||
lastDataFetchTime = NTP.getTime();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Fall-through to exit thread...
|
||||
@@ -127,7 +170,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
this.interrupt();
|
||||
}
|
||||
|
||||
private void processNames() {
|
||||
private void processNames() throws InterruptedException {
|
||||
// Fetch latest list of followed names
|
||||
List<String> followedNames = ResourceListManager.getInstance().getStringsInList("followedNames");
|
||||
if (followedNames == null || followedNames.isEmpty()) {
|
||||
@@ -140,11 +183,11 @@ public class ArbitraryDataManager extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private void processAll() {
|
||||
private void processAll() throws InterruptedException {
|
||||
this.fetchAndProcessTransactions(null);
|
||||
}
|
||||
|
||||
private void fetchAndProcessTransactions(String name) {
|
||||
private void fetchAndProcessTransactions(String name) throws InterruptedException {
|
||||
ArbitraryDataStorageManager storageManager = ArbitraryDataStorageManager.getInstance();
|
||||
|
||||
// Paginate queries when fetching arbitrary transactions
|
||||
@@ -152,6 +195,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
int offset = 0;
|
||||
|
||||
while (!isStopping) {
|
||||
Thread.sleep(1000L);
|
||||
|
||||
// Any arbitrary transactions we want to fetch data for?
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -166,6 +210,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
// Loop through signatures and remove ones we don't need to process
|
||||
Iterator iterator = signatures.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Thread.sleep(25L); // Reduce CPU usage
|
||||
byte[] signature = (byte[]) iterator.next();
|
||||
|
||||
ArbitraryTransaction arbitraryTransaction = fetchTransaction(repository, signature);
|
||||
@@ -222,6 +267,85 @@ public class ArbitraryDataManager extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchAllMetadata() throws InterruptedException {
|
||||
ArbitraryDataStorageManager storageManager = ArbitraryDataStorageManager.getInstance();
|
||||
|
||||
// Paginate queries when fetching arbitrary transactions
|
||||
final int limit = 100;
|
||||
int offset = 0;
|
||||
|
||||
while (!isStopping) {
|
||||
Thread.sleep(1000L);
|
||||
|
||||
// Any arbitrary transactions we want to fetch data for?
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, ARBITRARY_TX_TYPE, null, null, null, ConfirmationStatus.BOTH, limit, offset, true);
|
||||
// LOGGER.trace("Found {} arbitrary transactions at offset: {}, limit: {}", signatures.size(), offset, limit);
|
||||
if (signatures == null || signatures.isEmpty()) {
|
||||
offset = 0;
|
||||
break;
|
||||
}
|
||||
offset += limit;
|
||||
|
||||
// Loop through signatures and remove ones we don't need to process
|
||||
Iterator iterator = signatures.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Thread.sleep(25L); // Reduce CPU usage
|
||||
byte[] signature = (byte[]) iterator.next();
|
||||
|
||||
ArbitraryTransaction arbitraryTransaction = fetchTransaction(repository, signature);
|
||||
if (arbitraryTransaction == null) {
|
||||
// Best not to process this one
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
|
||||
|
||||
// Skip transactions that are blocked
|
||||
if (storageManager.isBlocked(arbitraryTransactionData)) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove transactions that we already have local data for
|
||||
if (hasLocalMetadata(arbitraryTransaction)) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (signatures.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pick one at random
|
||||
final int index = new Random().nextInt(signatures.size());
|
||||
byte[] signature = signatures.get(index);
|
||||
|
||||
if (signature == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check to see if we have had a more recent PUT
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
boolean hasMoreRecentPutTransaction = ArbitraryTransactionUtils.hasMoreRecentPutTransaction(repository, arbitraryTransactionData);
|
||||
if (hasMoreRecentPutTransaction) {
|
||||
// There is a more recent PUT transaction than the one we are currently processing.
|
||||
// When a PUT is issued, it replaces any layers that would have been there before.
|
||||
// Therefore any data relating to this older transaction is no longer needed and we
|
||||
// shouldn't fetch it from the network.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ask our connected peers if they have metadata for this signature
|
||||
fetchMetadata(arbitraryTransactionData);
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when fetching arbitrary transaction data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ArbitraryTransaction fetchTransaction(final Repository repository, byte[] signature) {
|
||||
try {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
@@ -241,16 +365,48 @@ public class ArbitraryDataManager extends Thread {
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when checking arbitrary transaction's data is local", e);
|
||||
return true;
|
||||
return true; // Assume true for now, to avoid network spam on error
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasLocalMetadata(ArbitraryTransaction arbitraryTransaction) {
|
||||
try {
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) arbitraryTransaction.getTransactionData();
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
|
||||
if (metadataHash == null) {
|
||||
// This transaction doesn't have metadata associated with it, so return true to indicate that we have everything
|
||||
return true;
|
||||
}
|
||||
|
||||
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
|
||||
|
||||
return metadataFile.exists();
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when checking arbitrary transaction's metadata is local", e);
|
||||
return true; // Assume true for now, to avoid network spam on error
|
||||
}
|
||||
}
|
||||
|
||||
// Entrypoint to request new data from peers
|
||||
public boolean fetchData(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
return ArbitraryDataFileListManager.getInstance().fetchArbitraryDataFileList(arbitraryTransactionData);
|
||||
}
|
||||
|
||||
// Entrypoint to request new metadata from peers
|
||||
public ArbitraryDataTransactionMetadata fetchMetadata(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(
|
||||
arbitraryTransactionData.getName(),
|
||||
ArbitraryDataFile.ResourceIdType.NAME,
|
||||
arbitraryTransactionData.getService(),
|
||||
arbitraryTransactionData.getIdentifier()
|
||||
);
|
||||
return ArbitraryMetadataManager.getInstance().fetchMetadata(resource, true);
|
||||
}
|
||||
|
||||
|
||||
// Useful methods used by other parts of the app
|
||||
|
||||
@@ -275,6 +431,9 @@ public class ArbitraryDataManager extends Thread {
|
||||
|
||||
// Cleanup file request caches
|
||||
ArbitraryDataFileManager.getInstance().cleanupRequestCache(now);
|
||||
|
||||
// Clean up metadata request caches
|
||||
ArbitraryMetadataManager.getInstance().cleanupRequestCache(now);
|
||||
}
|
||||
|
||||
public boolean isResourceCached(ArbitraryDataResource resource) {
|
||||
@@ -362,95 +521,19 @@ public class ArbitraryDataManager extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Broadcast list of hosted signatures
|
||||
|
||||
public void broadcastHostedSignatureList() {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<ArbitraryTransactionData> hostedTransactions = ArbitraryDataStorageManager.getInstance().listAllHostedTransactions(repository, null, null);
|
||||
List<byte[]> hostedSignatures = hostedTransactions.stream().map(ArbitraryTransactionData::getSignature).collect(Collectors.toList());
|
||||
if (!hostedSignatures.isEmpty()) {
|
||||
// Broadcast the list, using null to represent our peer address
|
||||
LOGGER.info("Broadcasting list of hosted signatures...");
|
||||
Message arbitrarySignatureMessage = new ArbitrarySignaturesMessage(null, 0, hostedSignatures);
|
||||
Network.getInstance().broadcast(broadcastPeer -> arbitrarySignatureMessage);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Repository issue when fetching arbitrary transaction data for broadcast", e);
|
||||
private boolean createDataDirectory() {
|
||||
// Create the data directory if it doesn't exist
|
||||
String dataPath = Settings.getInstance().getDataPath();
|
||||
Path dataDirectory = Paths.get(dataPath);
|
||||
try {
|
||||
Files.createDirectories(dataDirectory);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Unable to create data directory");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Handle incoming arbitrary signatures messages
|
||||
|
||||
public void onNetworkArbitrarySignaturesMessage(Peer peer, Message message) {
|
||||
// Don't process if QDN is disabled
|
||||
if (!Settings.getInstance().isQdnEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Received arbitrary signature list from peer {}", peer);
|
||||
|
||||
ArbitrarySignaturesMessage arbitrarySignaturesMessage = (ArbitrarySignaturesMessage) message;
|
||||
List<byte[]> signatures = arbitrarySignaturesMessage.getSignatures();
|
||||
|
||||
String peerAddress = peer.getPeerData().getAddress().toString();
|
||||
if (arbitrarySignaturesMessage.getPeerAddress() != null && !arbitrarySignaturesMessage.getPeerAddress().isEmpty()) {
|
||||
// This message is about a different peer than the one that sent it
|
||||
peerAddress = arbitrarySignaturesMessage.getPeerAddress();
|
||||
}
|
||||
|
||||
boolean containsNewEntry = false;
|
||||
|
||||
// Synchronize peer data lookups to make this process thread safe. Otherwise we could broadcast
|
||||
// the same data multiple times, due to more than one thread processing the same message from different peers
|
||||
synchronized (this.peerDataLock) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (byte[] signature : signatures) {
|
||||
|
||||
// Check if a record already exists for this hash/host combination
|
||||
// The port is not checked here - only the host/ip - in order to avoid duplicates
|
||||
// from filling up the db due to dynamic/ephemeral ports
|
||||
ArbitraryPeerData existingEntry = repository.getArbitraryRepository()
|
||||
.getArbitraryPeerDataForSignatureAndHost(signature, peer.getPeerData().getAddress().getHost());
|
||||
|
||||
if (existingEntry == null) {
|
||||
// We haven't got a record of this mapping yet, so add it
|
||||
ArbitraryPeerData arbitraryPeerData = new ArbitraryPeerData(signature, peerAddress);
|
||||
repository.discardChanges();
|
||||
if (arbitraryPeerData.isPeerAddressValid()) {
|
||||
LOGGER.debug("Adding arbitrary peer: {} for signature {}", peerAddress, Base58.encode(signature));
|
||||
repository.getArbitraryRepository().save(arbitraryPeerData);
|
||||
repository.saveChanges();
|
||||
|
||||
// Remember that this data is new, so that it can be rebroadcast later
|
||||
containsNewEntry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If at least one signature in this batch was new to us, we should rebroadcast the message to the
|
||||
// network in case some peers haven't received it yet
|
||||
if (containsNewEntry) {
|
||||
int requestHops = arbitrarySignaturesMessage.getRequestHops();
|
||||
arbitrarySignaturesMessage.setRequestHops(++requestHops);
|
||||
if (requestHops < ARBITRARY_SIGNATURES_REQUEST_MAX_HOPS) {
|
||||
LOGGER.debug("Rebroadcasting arbitrary signature list for peer {}. requestHops: {}", peerAddress, requestHops);
|
||||
Network.getInstance().broadcast(broadcastPeer -> broadcastPeer == peer ? null : arbitrarySignaturesMessage);
|
||||
}
|
||||
} else {
|
||||
// Don't rebroadcast as otherwise we could get into a loop
|
||||
}
|
||||
|
||||
// If anything needed saving, it would already have called saveChanges() above
|
||||
repository.discardChanges();
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while processing arbitrary transaction signature list from peer %s", peer), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int getPowDifficulty() {
|
||||
return this.powDifficulty;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class ArbitraryDataRenderManager extends Thread {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Arbitrary Data Manager");
|
||||
Thread.currentThread().setName("Arbitrary Data Render Manager");
|
||||
|
||||
try {
|
||||
while (!isStopping) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.qortal.utils.FilesystemUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -46,6 +47,9 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
|
||||
private List<ArbitraryTransactionData> hostedTransactions;
|
||||
|
||||
private String searchQuery;
|
||||
private List<ArbitraryTransactionData> searchResultsTransactions;
|
||||
|
||||
private static final long DIRECTORY_SIZE_CHECK_INTERVAL = 10 * 60 * 1000L; // 10 minutes
|
||||
|
||||
/** Treat storage as full at 90% usage, to reduce risk of going over the limit.
|
||||
@@ -225,6 +229,16 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data relating to a transaction is blocked by this node.
|
||||
*
|
||||
* @param arbitraryTransactionData - the transaction
|
||||
* @return boolean - whether the resource is blocked or not
|
||||
*/
|
||||
public boolean isBlocked(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
return isNameBlocked(arbitraryTransactionData.getName());
|
||||
}
|
||||
|
||||
private boolean isDataTypeAllowed(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
byte[] secret = arbitraryTransactionData.getSecret();
|
||||
boolean hasSecret = (secret != null && secret.length == 32);
|
||||
@@ -257,14 +271,8 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
}
|
||||
|
||||
|
||||
// Hosted data
|
||||
|
||||
public List<ArbitraryTransactionData> listAllHostedTransactions(Repository repository, Integer limit, Integer offset) {
|
||||
// Load from cache if we can, to avoid disk reads
|
||||
if (this.hostedTransactions != null) {
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.hostedTransactions, limit, offset);
|
||||
}
|
||||
|
||||
public List<ArbitraryTransactionData> loadAllHostedTransactions(Repository repository) {
|
||||
|
||||
List<ArbitraryTransactionData> arbitraryTransactionDataList = new ArrayList<>();
|
||||
|
||||
// Find all hosted paths
|
||||
@@ -285,7 +293,21 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
if (transactionData == null || transactionData.getType() != Transaction.TransactionType.ARBITRARY) {
|
||||
continue;
|
||||
}
|
||||
arbitraryTransactionDataList.add((ArbitraryTransactionData) transactionData);
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
|
||||
// Make sure to exclude metadata-only resources
|
||||
if (arbitraryTransactionData.getMetadataHash() != null) {
|
||||
if (contents.length == 1) {
|
||||
String metadataHash58 = Base58.encode(arbitraryTransactionData.getMetadataHash());
|
||||
if (Objects.equals(metadataHash58, contents[0])) {
|
||||
// We only have the metadata file for this resource, not the actual data, so exclude it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Found some data matching a transaction, so add it to the list
|
||||
arbitraryTransactionDataList.add(arbitraryTransactionData);
|
||||
|
||||
} catch (DataException e) {
|
||||
continue;
|
||||
@@ -295,10 +317,69 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
// Sort by newest first
|
||||
arbitraryTransactionDataList.sort(Comparator.comparingLong(ArbitraryTransactionData::getTimestamp).reversed());
|
||||
|
||||
// Update cache
|
||||
this.hostedTransactions = arbitraryTransactionDataList;
|
||||
return arbitraryTransactionDataList;
|
||||
}
|
||||
// Hosted data
|
||||
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(arbitraryTransactionDataList, limit, offset);
|
||||
public List<ArbitraryTransactionData> listAllHostedTransactions(Repository repository, Integer limit, Integer offset) {
|
||||
// Load from cache if we can, to avoid disk reads
|
||||
|
||||
if (this.hostedTransactions != null) {
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.hostedTransactions, limit, offset);
|
||||
}
|
||||
|
||||
this.hostedTransactions = this.loadAllHostedTransactions(repository);
|
||||
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.hostedTransactions, limit, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* searchHostedTransactions
|
||||
* Allow to run a query against hosted data names and return matches if there are any
|
||||
* @param repository
|
||||
* @param query
|
||||
* @param limit
|
||||
* @param offset
|
||||
* @return
|
||||
*/
|
||||
|
||||
public List<ArbitraryTransactionData> searchHostedTransactions(Repository repository, String query, Integer limit, Integer offset) {
|
||||
// Load from results cache if we can (results that exists for the same query), to avoid disk reads
|
||||
if (this.searchResultsTransactions != null && this.searchQuery.equals(query.toLowerCase())) {
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
|
||||
}
|
||||
|
||||
// Using cache if we can, to avoid disk reads
|
||||
if (this.hostedTransactions == null) {
|
||||
this.hostedTransactions = this.loadAllHostedTransactions(repository);
|
||||
}
|
||||
|
||||
this.searchQuery = query.toLowerCase(); //set the searchQuery so that it can be checked on the next call
|
||||
|
||||
List<ArbitraryTransactionData> searchResultsList = new ArrayList<>();
|
||||
|
||||
// Loop through cached hostedTransactions
|
||||
for (ArbitraryTransactionData atd : this.hostedTransactions) {
|
||||
try {
|
||||
if (atd.getName() != null && atd.getName().toLowerCase().contains(this.searchQuery)) {
|
||||
searchResultsList.add(atd);
|
||||
}
|
||||
else if (atd.getIdentifier() != null && atd.getIdentifier().toLowerCase().contains(this.searchQuery)) {
|
||||
searchResultsList.add(atd);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by newest first
|
||||
searchResultsList.sort(Comparator.comparingLong(ArbitraryTransactionData::getTimestamp).reversed());
|
||||
|
||||
// Update cache
|
||||
this.searchResultsTransactions = searchResultsList;
|
||||
|
||||
return ArbitraryTransactionUtils.limitOffsetTransactions(this.searchResultsTransactions, limit, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,7 +403,7 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
&& path.getFileName().toString().length() > 32)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
catch (IOException e) {
|
||||
catch (IOException | UncheckedIOException e) {
|
||||
LOGGER.info("Unable to walk through hosted data: {}", e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
package org.qortal.controller.arbitrary;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.controller.Controller;
|
||||
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.network.message.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.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;
|
||||
|
||||
public class ArbitraryMetadataManager {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryMetadataManager.class);
|
||||
|
||||
private static ArbitraryMetadataManager instance;
|
||||
|
||||
/**
|
||||
* Map of recent incoming requests for ARBITRARY transaction metadata.
|
||||
* <p>
|
||||
* Key is original request's message ID<br>
|
||||
* Value is Triple<transaction signature in base58, first requesting peer, first request's timestamp>
|
||||
* <p>
|
||||
* If peer is null then either:<br>
|
||||
* <ul>
|
||||
* <li>we are the original requesting peer</li>
|
||||
* <li>we have already sent data payload to original requesting peer.</li>
|
||||
* </ul>
|
||||
* If signature is null then we have already received the file list and either:<br>
|
||||
* <ul>
|
||||
* <li>we are the original requesting peer and have processed it</li>
|
||||
* <li>we have forwarded the metadata</li>
|
||||
* </ul>
|
||||
*/
|
||||
public Map<Integer, Triple<String, Peer, Long>> arbitraryMetadataRequests = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
/**
|
||||
* Map to keep track of in progress arbitrary metadata requests
|
||||
* Key: string - the signature encoded in base58
|
||||
* Value: Triple<networkBroadcastCount, directPeerRequestCount, lastAttemptTimestamp>
|
||||
*/
|
||||
private Map<String, Triple<Integer, Integer, Long>> arbitraryMetadataSignatureRequests = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
|
||||
private ArbitraryMetadataManager() {
|
||||
}
|
||||
|
||||
public static ArbitraryMetadataManager getInstance() {
|
||||
if (instance == null)
|
||||
instance = new ArbitraryMetadataManager();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void cleanupRequestCache(Long now) {
|
||||
if (now == null) {
|
||||
return;
|
||||
}
|
||||
final long requestMinimumTimestamp = now - ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT;
|
||||
arbitraryMetadataRequests.entrySet().removeIf(entry -> entry.getValue().getC() == null || entry.getValue().getC() < requestMinimumTimestamp);
|
||||
}
|
||||
|
||||
|
||||
public ArbitraryDataTransactionMetadata fetchMetadata(ArbitraryDataResource arbitraryDataResource, boolean useRateLimiter) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
// Find latest transaction
|
||||
ArbitraryTransactionData latestTransaction = repository.getArbitraryRepository()
|
||||
.getLatestTransaction(arbitraryDataResource.getResourceId(), arbitraryDataResource.getService(),
|
||||
null, arbitraryDataResource.getIdentifier());
|
||||
|
||||
if (latestTransaction != null) {
|
||||
byte[] signature = latestTransaction.getSignature();
|
||||
byte[] metadataHash = latestTransaction.getMetadataHash();
|
||||
if (metadataHash == null) {
|
||||
// This resource doesn't have metadata
|
||||
throw new IllegalArgumentException("This resource doesn't have metadata");
|
||||
}
|
||||
|
||||
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
|
||||
if (!metadataFile.exists()) {
|
||||
// Request from network
|
||||
this.fetchArbitraryMetadata(latestTransaction, useRateLimiter);
|
||||
}
|
||||
|
||||
// Now check again as it may have been downloaded above
|
||||
if (metadataFile.exists()) {
|
||||
// Use local copy
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = new ArbitraryDataTransactionMetadata(metadataFile.getFilePath());
|
||||
transactionMetadata.read();
|
||||
return transactionMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException | IOException e) {
|
||||
LOGGER.error("Repository issue when fetching arbitrary transaction metadata", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Request metadata from network
|
||||
|
||||
public byte[] fetchArbitraryMetadata(ArbitraryTransactionData arbitraryTransactionData, boolean useRateLimiter) {
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
if (metadataHash == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
|
||||
// Require an NTP sync
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we've already tried too many times in a short space of time, make sure to give up
|
||||
if (useRateLimiter && !this.shouldMakeMetadataRequestForSignature(signature58)) {
|
||||
LOGGER.trace("Skipping metadata request for signature {} due to rate limit", signature58);
|
||||
return null;
|
||||
}
|
||||
this.addToSignatureRequests(signature58, true, false);
|
||||
|
||||
List<Peer> handshakedPeers = Network.getInstance().getImmutableHandshakedPeers();
|
||||
LOGGER.debug(String.format("Sending metadata request for signature %s to %d peers...", signature58, handshakedPeers.size()));
|
||||
|
||||
// Build request
|
||||
Message getArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, now, 0);
|
||||
|
||||
// Save our request into requests map
|
||||
Triple<String, Peer, Long> requestEntry = new Triple<>(signature58, null, NTP.getTime());
|
||||
|
||||
// Assign random ID to this message
|
||||
int id;
|
||||
do {
|
||||
id = new Random().nextInt(Integer.MAX_VALUE - 1) + 1;
|
||||
|
||||
// Put queue into map (keyed by message ID) so we can poll for a response
|
||||
// If putIfAbsent() doesn't return null, then this ID is already taken
|
||||
} while (arbitraryMetadataRequests.put(id, requestEntry) != null);
|
||||
getArbitraryMetadataMessage.setId(id);
|
||||
|
||||
// Broadcast request
|
||||
Network.getInstance().broadcast(peer -> getArbitraryMetadataMessage);
|
||||
|
||||
// Poll to see if data has arrived
|
||||
final long singleWait = 100;
|
||||
long totalWait = 0;
|
||||
while (totalWait < ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT) {
|
||||
try {
|
||||
Thread.sleep(singleWait);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
requestEntry = arbitraryMetadataRequests.get(id);
|
||||
if (requestEntry == null)
|
||||
return null;
|
||||
|
||||
if (requestEntry.getA() == null)
|
||||
break;
|
||||
|
||||
totalWait += singleWait;
|
||||
}
|
||||
|
||||
try {
|
||||
ArbitraryDataFile metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
|
||||
if (metadataFile.exists()) {
|
||||
return metadataFile.getBytes();
|
||||
}
|
||||
} catch (DataException e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Track metadata lookups by signature
|
||||
|
||||
private boolean shouldMakeMetadataRequestForSignature(String signature58) {
|
||||
Triple<Integer, Integer, Long> request = arbitraryMetadataSignatureRequests.get(signature58);
|
||||
|
||||
if (request == null) {
|
||||
// Not attempted yet
|
||||
return true;
|
||||
}
|
||||
|
||||
// Extract the components
|
||||
Integer networkBroadcastCount = request.getA();
|
||||
// Integer directPeerRequestCount = request.getB();
|
||||
Long lastAttemptTimestamp = request.getC();
|
||||
|
||||
if (lastAttemptTimestamp == null) {
|
||||
// Not attempted yet
|
||||
return true;
|
||||
}
|
||||
|
||||
long timeSinceLastAttempt = NTP.getTime() - lastAttemptTimestamp;
|
||||
|
||||
// Allow a second attempt after 60 seconds
|
||||
if (timeSinceLastAttempt > 60 * 1000L) {
|
||||
// We haven't tried for at least 60 seconds
|
||||
|
||||
if (networkBroadcastCount < 2) {
|
||||
// We've made less than 2 total attempts
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Then allow another attempt after 60 minutes
|
||||
if (timeSinceLastAttempt > 60 * 60 * 1000L) {
|
||||
// We haven't tried for at least 60 minutes
|
||||
|
||||
if (networkBroadcastCount < 3) {
|
||||
// We've made less than 3 total attempts
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isSignatureRateLimited(byte[] signature) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
return !this.shouldMakeMetadataRequestForSignature(signature58);
|
||||
}
|
||||
|
||||
public long lastRequestForSignature(byte[] signature) {
|
||||
String signature58 = Base58.encode(signature);
|
||||
Triple<Integer, Integer, Long> request = arbitraryMetadataSignatureRequests.get(signature58);
|
||||
|
||||
if (request == null) {
|
||||
// Not attempted yet
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Extract the components
|
||||
Long lastAttemptTimestamp = request.getC();
|
||||
if (lastAttemptTimestamp != null) {
|
||||
return lastAttemptTimestamp;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void addToSignatureRequests(String signature58, boolean incrementNetworkRequests, boolean incrementPeerRequests) {
|
||||
Triple<Integer, Integer, Long> request = arbitraryMetadataSignatureRequests.get(signature58);
|
||||
Long now = NTP.getTime();
|
||||
|
||||
if (request == null) {
|
||||
// No entry yet
|
||||
Triple<Integer, Integer, Long> newRequest = new Triple<>(0, 0, now);
|
||||
arbitraryMetadataSignatureRequests.put(signature58, newRequest);
|
||||
}
|
||||
else {
|
||||
// There is an existing entry
|
||||
if (incrementNetworkRequests) {
|
||||
request.setA(request.getA() + 1);
|
||||
}
|
||||
if (incrementPeerRequests) {
|
||||
request.setB(request.getB() + 1);
|
||||
}
|
||||
request.setC(now);
|
||||
arbitraryMetadataSignatureRequests.put(signature58, request);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFromSignatureRequests(String signature58) {
|
||||
arbitraryMetadataSignatureRequests.remove(signature58);
|
||||
}
|
||||
|
||||
|
||||
// Network handlers
|
||||
|
||||
public void onNetworkArbitraryMetadataMessage(Peer peer, Message message) {
|
||||
// Don't process if QDN is disabled
|
||||
if (!Settings.getInstance().isQdnEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArbitraryMetadataMessage arbitraryMetadataMessage = (ArbitraryMetadataMessage) message;
|
||||
LOGGER.debug("Received metadata from peer {}", peer);
|
||||
|
||||
// Do we have a pending request for this data?
|
||||
Triple<String, Peer, Long> request = arbitraryMetadataRequests.get(message.getId());
|
||||
if (request == null || request.getA() == null) {
|
||||
return;
|
||||
}
|
||||
boolean isRelayRequest = (request.getB() != null);
|
||||
|
||||
// Does this message's signature match what we're expecting?
|
||||
byte[] signature = arbitraryMetadataMessage.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
if (!request.getA().equals(signature58)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update requests map to reflect that we've received all chunks
|
||||
Triple<String, Peer, Long> newEntry = new Triple<>(null, null, request.getC());
|
||||
arbitraryMetadataRequests.put(message.getId(), newEntry);
|
||||
|
||||
ArbitraryTransactionData arbitraryTransactionData = null;
|
||||
|
||||
// Forwarding
|
||||
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
|
||||
|
||||
// Get transaction info
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
if (!(transactionData instanceof ArbitraryTransactionData))
|
||||
return;
|
||||
arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while finding arbitrary transaction metadata for peer %s", peer), e);
|
||||
}
|
||||
|
||||
// Check if the name is blocked
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ArbitraryDataStorageManager.getInstance().isNameBlocked(arbitraryTransactionData.getName()));
|
||||
if (!isBlocked) {
|
||||
Peer requestingPeer = request.getB();
|
||||
if (requestingPeer != null) {
|
||||
|
||||
ArbitraryMetadataMessage forwardArbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, arbitraryMetadataMessage.getArbitraryMetadataFile());
|
||||
|
||||
// Forward to requesting peer
|
||||
LOGGER.debug("Forwarding metadata to requesting peer: {}", requestingPeer);
|
||||
if (!requestingPeer.sendMessage(forwardArbitraryMetadataMessage)) {
|
||||
requestingPeer.disconnect("failed to forward arbitrary metadata");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onNetworkGetArbitraryMetadataMessage(Peer peer, Message message) {
|
||||
// Don't respond if QDN is disabled
|
||||
if (!Settings.getInstance().isQdnEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Controller.getInstance().stats.getArbitraryMetadataMessageStats.requests.incrementAndGet();
|
||||
|
||||
GetArbitraryMetadataMessage getArbitraryMetadataMessage = (GetArbitraryMetadataMessage) message;
|
||||
byte[] signature = getArbitraryMetadataMessage.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
Long now = NTP.getTime();
|
||||
Triple<String, Peer, Long> newEntry = new Triple<>(signature58, peer, now);
|
||||
|
||||
// If we've seen this request recently, then ignore
|
||||
if (arbitraryMetadataRequests.putIfAbsent(message.getId(), newEntry) != null) {
|
||||
LOGGER.debug("Ignoring metadata request from peer {} for signature {}", peer, signature58);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Received metadata request from peer {} for signature {}", peer, signature58);
|
||||
|
||||
ArbitraryTransactionData transactionData = null;
|
||||
ArbitraryDataFile metadataFile = null;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Firstly we need to lookup this file on chain to get its metadata hash
|
||||
transactionData = (ArbitraryTransactionData)repository.getTransactionRepository().fromSignature(signature);
|
||||
if (transactionData instanceof ArbitraryTransactionData) {
|
||||
|
||||
// Check if we're even allowed to serve metadata for this transaction
|
||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
||||
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
if (metadataHash != null) {
|
||||
|
||||
// Load metadata file
|
||||
metadataFile = ArbitraryDataFile.fromHash(metadataHash, signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while fetching arbitrary metadata for peer %s", peer), e);
|
||||
}
|
||||
|
||||
// We should only respond if we have the metadata file
|
||||
if (metadataFile != null && metadataFile.exists()) {
|
||||
|
||||
// We have the metadata file, so update requests map to reflect that we've sent it
|
||||
newEntry = new Triple<>(null, null, now);
|
||||
arbitraryMetadataRequests.put(message.getId(), newEntry);
|
||||
|
||||
ArbitraryMetadataMessage arbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, metadataFile);
|
||||
arbitraryMetadataMessage.setId(message.getId());
|
||||
if (!peer.sendMessage(arbitraryMetadataMessage)) {
|
||||
LOGGER.debug("Couldn't send metadata");
|
||||
peer.disconnect("failed to send metadata");
|
||||
return;
|
||||
}
|
||||
LOGGER.debug("Sent metadata");
|
||||
|
||||
// Nothing left to do, so return to prevent any unnecessary forwarding from occurring
|
||||
LOGGER.debug("No need for any forwarding because metadata request is fully served");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// We may need to forward this request on
|
||||
boolean isBlocked = (transactionData == null || ArbitraryDataStorageManager.getInstance().isNameBlocked(transactionData.getName()));
|
||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
long requestTime = getArbitraryMetadataMessage.getRequestTime();
|
||||
int requestHops = getArbitraryMetadataMessage.getRequestHops() + 1;
|
||||
long totalRequestTime = now - requestTime;
|
||||
|
||||
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
|
||||
// Relay request hasn't timed out yet, so can potentially be rebroadcast
|
||||
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
|
||||
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
|
||||
|
||||
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
|
||||
|
||||
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
|
||||
Network.getInstance().broadcast(
|
||||
broadcastPeer -> broadcastPeer == peer ||
|
||||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
|
||||
? null : relayGetArbitraryMetadataMessage);
|
||||
|
||||
}
|
||||
else {
|
||||
// This relay request has reached the maximum number of allowed hops
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This relay request has timed out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.qortal.controller.repository;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -47,7 +48,7 @@ public class AtStatesPruner implements Runnable {
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing())
|
||||
if (Synchronizer.getInstance().isSynchronizing())
|
||||
continue;
|
||||
|
||||
// Prune AT states for all blocks up until our latest minus pruneBlockLimit
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.qortal.controller.repository;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -34,7 +35,7 @@ public class AtStatesTrimmer implements Runnable {
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing())
|
||||
if (Synchronizer.getInstance().isSynchronizing())
|
||||
continue;
|
||||
|
||||
long currentTrimmableTimestamp = NTP.getTime() - Settings.getInstance().getAtStatesMaxLifetime();
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.qortal.controller.repository;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.settings.Settings;
|
||||
@@ -51,7 +52,7 @@ public class BlockArchiver implements Runnable {
|
||||
}
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing()) {
|
||||
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.qortal.controller.repository;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -51,7 +52,7 @@ public class BlockPruner implements Runnable {
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing()) {
|
||||
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ public class NamesDatabaseIntegrityCheck {
|
||||
private List<TransactionData> nameTransactions = new ArrayList<>();
|
||||
|
||||
public int rebuildName(String name, Repository repository) {
|
||||
return this.rebuildName(name, repository, null);
|
||||
}
|
||||
|
||||
public int rebuildName(String name, Repository repository, List<String> referenceNames) {
|
||||
// "referenceNames" tracks the linked names that have already been rebuilt, to prevent circular dependencies
|
||||
if (referenceNames == null) {
|
||||
referenceNames = new ArrayList<>();
|
||||
}
|
||||
|
||||
int modificationCount = 0;
|
||||
try {
|
||||
List<TransactionData> transactions = this.fetchAllTransactionsInvolvingName(name, repository);
|
||||
@@ -56,7 +65,14 @@ public class NamesDatabaseIntegrityCheck {
|
||||
if (Objects.equals(updateNameTransactionData.getNewName(), name) &&
|
||||
!Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName())) {
|
||||
// This renames an existing name, so we need to process that instead
|
||||
this.rebuildName(updateNameTransactionData.getName(), repository);
|
||||
|
||||
if (!referenceNames.contains(name)) {
|
||||
referenceNames.add(name);
|
||||
this.rebuildName(updateNameTransactionData.getName(), repository, referenceNames);
|
||||
}
|
||||
else {
|
||||
// We've already processed this name so there's nothing more to do
|
||||
}
|
||||
}
|
||||
else {
|
||||
Name nameObj = new Name(repository, name);
|
||||
@@ -193,7 +209,12 @@ public class NamesDatabaseIntegrityCheck {
|
||||
newName = registeredName;
|
||||
}
|
||||
NameData newNameData = repository.getNameRepository().fromName(newName);
|
||||
if (!Objects.equals(creator.getAddress(), newNameData.getOwner())) {
|
||||
if (newNameData == null) {
|
||||
LOGGER.info("Error: registered name {} has no new name data. This is likely due to account {} " +
|
||||
"being renamed another time, which is a scenario that is not yet checked automatically.",
|
||||
updateNameTransactionData.getNewName(), creator.getAddress());
|
||||
}
|
||||
else if (!Objects.equals(creator.getAddress(), newNameData.getOwner())) {
|
||||
LOGGER.info("Error: registered name {} is owned by {}, but it should be {}",
|
||||
updateNameTransactionData.getNewName(), newNameData.getOwner(), creator.getAddress());
|
||||
integrityCheckFailed = true;
|
||||
@@ -268,42 +289,6 @@ public class NamesDatabaseIntegrityCheck {
|
||||
return registerNameTransactions;
|
||||
}
|
||||
|
||||
private List<UpdateNameTransactionData> fetchUpdateNameTransactions() {
|
||||
List<UpdateNameTransactionData> updateNameTransactions = new ArrayList<>();
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
if (transactionData.getType() == TransactionType.UPDATE_NAME) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
||||
updateNameTransactions.add(updateNameTransactionData);
|
||||
}
|
||||
}
|
||||
return updateNameTransactions;
|
||||
}
|
||||
|
||||
private List<SellNameTransactionData> fetchSellNameTransactions() {
|
||||
List<SellNameTransactionData> sellNameTransactions = new ArrayList<>();
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
if (transactionData.getType() == TransactionType.SELL_NAME) {
|
||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
|
||||
sellNameTransactions.add(sellNameTransactionData);
|
||||
}
|
||||
}
|
||||
return sellNameTransactions;
|
||||
}
|
||||
|
||||
private List<BuyNameTransactionData> fetchBuyNameTransactions() {
|
||||
List<BuyNameTransactionData> buyNameTransactions = new ArrayList<>();
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
if (transactionData.getType() == TransactionType.BUY_NAME) {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
|
||||
buyNameTransactions.add(buyNameTransactionData);
|
||||
}
|
||||
}
|
||||
return buyNameTransactions;
|
||||
}
|
||||
|
||||
private void fetchAllNameTransactions(Repository repository) throws DataException {
|
||||
List<TransactionData> nameTransactions = new ArrayList<>();
|
||||
|
||||
@@ -319,43 +304,40 @@ public class NamesDatabaseIntegrityCheck {
|
||||
this.nameTransactions = nameTransactions;
|
||||
}
|
||||
|
||||
private List<TransactionData> fetchAllTransactionsInvolvingName(String name, Repository repository) throws DataException {
|
||||
List<TransactionData> transactions = new ArrayList<>();
|
||||
public List<TransactionData> fetchAllTransactionsInvolvingName(String name, Repository repository) throws DataException {
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
String reducedName = Unicode.sanitize(name);
|
||||
|
||||
// Fetch all the confirmed name-modification transactions
|
||||
if (this.nameTransactions.isEmpty()) {
|
||||
this.fetchAllNameTransactions(repository);
|
||||
List<byte[]> registerNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||
TransactionType.REGISTER_NAME, Arrays.asList("(name = ? OR reduced_name = ?)"), Arrays.asList(name, reducedName));
|
||||
signatures.addAll(registerNameTransactions);
|
||||
|
||||
List<byte[]> updateNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||
TransactionType.UPDATE_NAME,
|
||||
Arrays.asList("(name = ? OR new_name = ? OR (reduced_new_name != '' AND reduced_new_name = ?))"),
|
||||
Arrays.asList(name, name, reducedName));
|
||||
signatures.addAll(updateNameTransactions);
|
||||
|
||||
List<byte[]> sellNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||
TransactionType.SELL_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
||||
signatures.addAll(sellNameTransactions);
|
||||
|
||||
List<byte[]> buyNameTransactions = repository.getTransactionRepository().getSignaturesMatchingCustomCriteria(
|
||||
TransactionType.BUY_NAME, Arrays.asList("name = ?"), Arrays.asList(name));
|
||||
signatures.addAll(buyNameTransactions);
|
||||
|
||||
List<TransactionData> transactions = new ArrayList<>();
|
||||
for (byte[] signature : signatures) {
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||
// Filter out any unconfirmed transactions
|
||||
if (transactionData.getBlockHeight() != null && transactionData.getBlockHeight() > 0) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
|
||||
for (TransactionData transactionData : this.nameTransactions) {
|
||||
// Sort by lowest timestamp first
|
||||
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp));
|
||||
|
||||
if ((transactionData instanceof RegisterNameTransactionData)) {
|
||||
RegisterNameTransactionData registerNameTransactionData = (RegisterNameTransactionData) transactionData;
|
||||
if (Objects.equals(registerNameTransactionData.getReducedName(), reducedName)) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof UpdateNameTransactionData)) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
||||
if (Objects.equals(updateNameTransactionData.getName(), name) ||
|
||||
Objects.equals(updateNameTransactionData.getReducedNewName(), reducedName)) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof BuyNameTransactionData)) {
|
||||
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) transactionData;
|
||||
if (Objects.equals(buyNameTransactionData.getName(), name)) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
if ((transactionData instanceof SellNameTransactionData)) {
|
||||
SellNameTransactionData sellNameTransactionData = (SellNameTransactionData) transactionData;
|
||||
if (Objects.equals(sellNameTransactionData.getName(), name)) {
|
||||
transactions.add(transactionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -36,7 +37,7 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
continue;
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Controller.getInstance().isSynchronizing())
|
||||
if (Synchronizer.getInstance().isSynchronizing())
|
||||
continue;
|
||||
|
||||
// Trim blockchain by removing 'old' online accounts signatures
|
||||
|
||||
@@ -2,41 +2,38 @@ package org.qortal.controller.tradebot;
|
||||
|
||||
import java.awt.TrayIcon.MessageType;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.util.Supplier;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.PresenceTransactionData;
|
||||
import org.qortal.data.network.TradePresenceData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.gui.SysTray;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.network.Peer;
|
||||
import org.qortal.network.message.GetTradePresencesMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.TradePresencesMessage;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.repository.hsqldb.HSQLDBImportExport;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.PresenceTransaction;
|
||||
import org.qortal.transaction.PresenceTransaction.PresenceType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.ByteArray;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
@@ -56,6 +53,15 @@ public class TradeBot implements Listener {
|
||||
private static final Logger LOGGER = LogManager.getLogger(TradeBot.class);
|
||||
private static final Random RANDOM = new SecureRandom();
|
||||
|
||||
/** Maximum lifetime of trade presence timestamp. 30 mins in ms. */
|
||||
private static final long PRESENCE_LIFETIME = 30 * 60 * 1000L;
|
||||
/** How soon before expiry of our own trade presence timestamp that we want to trigger renewal. 5 mins in ms. */
|
||||
private static final long EARLY_RENEWAL_PERIOD = 5 * 60 * 1000L;
|
||||
/** Trade presence timestamps are rounded up to this nearest interval. Bigger values improve grouping of entries in [GET_]TRADE_PRESENCES network messages. 15 mins in ms. */
|
||||
private static final long EXPIRY_ROUNDING = 15 * 60 * 1000L;
|
||||
/** How often we want to broadcast our list of all known trade presences to peers. 5 mins in ms. */
|
||||
private static final long PRESENCE_BROADCAST_INTERVAL = 5 * 60 * 1000L;
|
||||
|
||||
public interface StateNameAndValueSupplier {
|
||||
public String getState();
|
||||
public int getStateValue();
|
||||
@@ -73,6 +79,18 @@ public class TradeBot implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
public static class TradePresenceEvent implements Event {
|
||||
private final TradePresenceData tradePresenceData;
|
||||
|
||||
public TradePresenceEvent(TradePresenceData tradePresenceData) {
|
||||
this.tradePresenceData = tradePresenceData;
|
||||
}
|
||||
|
||||
public TradePresenceData getTradePresenceData() {
|
||||
return this.tradePresenceData;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<Class<? extends ACCT>, Supplier<AcctTradeBot>> acctTradeBotSuppliers = new HashMap<>();
|
||||
static {
|
||||
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
|
||||
@@ -86,7 +104,12 @@ public class TradeBot implements Listener {
|
||||
|
||||
private static TradeBot instance;
|
||||
|
||||
private final Map<String, Long> presenceTimestampsByAtAddress = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Map<ByteArray, Long> ourTradePresenceTimestampsByPubkey = Collections.synchronizedMap(new HashMap<>());
|
||||
private final List<TradePresenceData> pendingTradePresences = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private final Map<ByteArray, TradePresenceData> allTradePresencesByPubkey = Collections.synchronizedMap(new HashMap<>());
|
||||
private Map<ByteArray, TradePresenceData> safeAllTradePresencesByPubkey = Collections.emptyMap();
|
||||
private long nextTradePresenceBroadcastTimestamp = 0L;
|
||||
|
||||
private TradeBot() {
|
||||
EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
|
||||
@@ -213,10 +236,17 @@ public class TradeBot implements Listener {
|
||||
|
||||
@Override
|
||||
public void listen(Event event) {
|
||||
if (!(event instanceof Controller.NewBlockEvent))
|
||||
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);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
|
||||
return;
|
||||
|
||||
synchronized (this) {
|
||||
expireOldPresenceTimestamps();
|
||||
|
||||
List<TradeBotData> allTradeBotData;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -247,6 +277,8 @@ public class TradeBot implements Listener {
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.warn(() -> String.format("Foreign blockchain issue processing trade-bot entry for AT %s: %s", tradeBotData.getAtAddress(), e.getMessage()));
|
||||
}
|
||||
|
||||
broadcastPresenceTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +356,33 @@ public class TradeBot implements Listener {
|
||||
}
|
||||
|
||||
// PRESENCE-related
|
||||
|
||||
public Collection<TradePresenceData> getAllTradePresences() {
|
||||
return this.safeAllTradePresencesByPubkey.values();
|
||||
}
|
||||
|
||||
/** Trade presence timestamps expire in the 'future' so any that reach 'now' have expired and are removed. */
|
||||
private void expireOldPresenceTimestamps() {
|
||||
long now = NTP.getTime();
|
||||
|
||||
int allRemovedCount = 0;
|
||||
synchronized (this.allTradePresencesByPubkey) {
|
||||
int preRemoveCount = this.allTradePresencesByPubkey.size();
|
||||
this.allTradePresencesByPubkey.values().removeIf(tradePresenceData -> tradePresenceData.getTimestamp() <= now);
|
||||
allRemovedCount = this.allTradePresencesByPubkey.size() - preRemoveCount;
|
||||
}
|
||||
|
||||
int ourRemovedCount = 0;
|
||||
synchronized (this.ourTradePresenceTimestampsByPubkey) {
|
||||
int preRemoveCount = this.ourTradePresenceTimestampsByPubkey.size();
|
||||
this.ourTradePresenceTimestampsByPubkey.values().removeIf(timestamp -> timestamp < now);
|
||||
ourRemovedCount = this.ourTradePresenceTimestampsByPubkey.size() - preRemoveCount;
|
||||
}
|
||||
|
||||
if (allRemovedCount > 0)
|
||||
LOGGER.debug("Removed {} expired trade presences, of which {} ours", allRemovedCount, ourRemovedCount);
|
||||
}
|
||||
|
||||
/*package*/ void updatePresence(Repository repository, TradeBotData tradeBotData, CrossChainTradeData tradeData)
|
||||
throws DataException {
|
||||
String atAddress = tradeBotData.getAtAddress();
|
||||
@@ -332,44 +391,292 @@ public class TradeBot implements Listener {
|
||||
String signerAddress = tradeNativeAccount.getAddress();
|
||||
|
||||
/*
|
||||
* There's no point in Alice trying to build a PRESENCE transaction
|
||||
* for an AT that isn't locked to her, as other peers won't be able
|
||||
* to validate the PRESENCE transaction as signing public key won't
|
||||
* be visible.
|
||||
*/
|
||||
if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress))
|
||||
// Signer is neither Bob, nor Alice, or trade not yet locked to Alice
|
||||
* There's no point in Alice trying to broadcast presence for an AT that isn't locked to her,
|
||||
* as other peers won't be able to verify as signing public key isn't yet in the AT's data segment.
|
||||
*/
|
||||
if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress)) {
|
||||
// Signer is neither Bob, nor trade locked to Alice
|
||||
LOGGER.trace("Can't provide trade presence for our AT {} as it's not yet locked to Alice", atAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
long now = NTP.getTime();
|
||||
long threshold = now - PresenceType.TRADE_BOT.getLifetime();
|
||||
long newExpiry = generateExpiry(now);
|
||||
ByteArray pubkeyByteArray = ByteArray.wrap(tradeNativeAccount.getPublicKey());
|
||||
|
||||
long timestamp = presenceTimestampsByAtAddress.compute(atAddress, (k, v) -> (v == null || v < threshold) ? now : v);
|
||||
// If map entry's timestamp is missing, or within early renewal period, use the new expiry - otherwise use existing timestamp.
|
||||
synchronized (this.ourTradePresenceTimestampsByPubkey) {
|
||||
Long currentTimestamp = this.ourTradePresenceTimestampsByPubkey.get(pubkeyByteArray);
|
||||
|
||||
// If timestamp hasn't been updated then nothing to do
|
||||
if (timestamp != now)
|
||||
if (currentTimestamp != null && currentTimestamp - now > EARLY_RENEWAL_PERIOD) {
|
||||
// timestamp still good
|
||||
LOGGER.trace("Current trade presence timestamp {} still good for our trade {}", currentTimestamp, atAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
this.ourTradePresenceTimestampsByPubkey.put(pubkeyByteArray, newExpiry);
|
||||
}
|
||||
|
||||
// Create signature
|
||||
byte[] signature = tradeNativeAccount.sign(Longs.toByteArray(newExpiry));
|
||||
|
||||
// Add new trade presence to queue to be broadcast around network
|
||||
TradePresenceData tradePresenceData = new TradePresenceData(newExpiry, tradeNativeAccount.getPublicKey(), signature, atAddress);
|
||||
this.pendingTradePresences.add(tradePresenceData);
|
||||
|
||||
this.allTradePresencesByPubkey.put(pubkeyByteArray, tradePresenceData);
|
||||
rebuildSafeAllTradePresences();
|
||||
|
||||
LOGGER.trace("New trade presence timestamp {} for our trade {}", newExpiry, atAddress);
|
||||
|
||||
EventBus.INSTANCE.notify(new TradePresenceEvent(tradePresenceData));
|
||||
}
|
||||
|
||||
private void rebuildSafeAllTradePresences() {
|
||||
synchronized (this.allTradePresencesByPubkey) {
|
||||
// Collect into a *new* unmodifiable map.
|
||||
this.safeAllTradePresencesByPubkey = Map.copyOf(this.allTradePresencesByPubkey);
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastPresenceTimestamps() {
|
||||
// If we have new trade presences that are pending broadcast, send those as a priority
|
||||
if (!this.pendingTradePresences.isEmpty()) {
|
||||
// Create a copy for Network to safely use in another thread
|
||||
List<TradePresenceData> safeTradePresences;
|
||||
synchronized (this.pendingTradePresences) {
|
||||
safeTradePresences = List.copyOf(this.pendingTradePresences);
|
||||
this.pendingTradePresences.clear();
|
||||
}
|
||||
|
||||
LOGGER.debug("Broadcasting {} new trade presences", safeTradePresences.size());
|
||||
|
||||
TradePresencesMessage tradePresencesMessage = new TradePresencesMessage(safeTradePresences);
|
||||
Network.getInstance().broadcast(peer -> tradePresencesMessage);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// As we have no new trade presences, check whether it's time to do a general broadcast
|
||||
Long now = NTP.getTime();
|
||||
if (now == null || now < nextTradePresenceBroadcastTimestamp)
|
||||
return;
|
||||
|
||||
int txGroupId = Group.NO_GROUP;
|
||||
byte[] reference = new byte[TransactionTransformer.SIGNATURE_LENGTH];
|
||||
byte[] creatorPublicKey = tradeNativeAccount.getPublicKey();
|
||||
long fee = 0L;
|
||||
nextTradePresenceBroadcastTimestamp = now + PRESENCE_BROADCAST_INTERVAL;
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, null);
|
||||
List<TradePresenceData> safeTradePresences = List.copyOf(this.safeAllTradePresencesByPubkey.values());
|
||||
|
||||
int nonce = 0;
|
||||
byte[] timestampSignature = tradeNativeAccount.sign(Longs.toByteArray(timestamp));
|
||||
if (safeTradePresences.isEmpty())
|
||||
return;
|
||||
|
||||
PresenceTransactionData transactionData = new PresenceTransactionData(baseTransactionData, nonce, PresenceType.TRADE_BOT, timestampSignature);
|
||||
LOGGER.debug("Broadcasting all {} known trade presences. Next broadcast timestamp: {}",
|
||||
safeTradePresences.size(), nextTradePresenceBroadcastTimestamp
|
||||
);
|
||||
|
||||
PresenceTransaction presenceTransaction = new PresenceTransaction(repository, transactionData);
|
||||
presenceTransaction.computeNonce();
|
||||
GetTradePresencesMessage getTradePresencesMessage = new GetTradePresencesMessage(safeTradePresences);
|
||||
Network.getInstance().broadcast(peer -> getTradePresencesMessage);
|
||||
}
|
||||
|
||||
presenceTransaction.sign(tradeNativeAccount);
|
||||
// Network message processing
|
||||
|
||||
ValidationResult result = presenceTransaction.importAsUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
LOGGER.debug(() -> String.format("Unable to build trade-bot PRESENCE transaction for %s: %s", tradeBotData.getAtAddress(), result.name()));
|
||||
public void onGetTradePresencesMessage(Peer peer, Message message) {
|
||||
GetTradePresencesMessage getTradePresencesMessage = (GetTradePresencesMessage) message;
|
||||
|
||||
List<TradePresenceData> peersTradePresences = getTradePresencesMessage.getTradePresences();
|
||||
|
||||
// Create mutable copy from safe snapshot
|
||||
Map<ByteArray, TradePresenceData> entriesUnknownToPeer = new HashMap<>(this.safeAllTradePresencesByPubkey);
|
||||
int knownCount = entriesUnknownToPeer.size();
|
||||
|
||||
for (TradePresenceData peersTradePresence : peersTradePresences) {
|
||||
ByteArray pubkeyByteArray = ByteArray.wrap(peersTradePresence.getPublicKey());
|
||||
|
||||
TradePresenceData ourEntry = entriesUnknownToPeer.get(pubkeyByteArray);
|
||||
|
||||
if (ourEntry != null && ourEntry.getTimestamp() == peersTradePresence.getTimestamp())
|
||||
entriesUnknownToPeer.remove(pubkeyByteArray);
|
||||
}
|
||||
|
||||
if (entriesUnknownToPeer.isEmpty())
|
||||
return;
|
||||
|
||||
LOGGER.debug("Sending {} trade presences to peer {} after excluding their {} from known {}",
|
||||
entriesUnknownToPeer.size(), peer, peersTradePresences.size(), knownCount
|
||||
);
|
||||
|
||||
// Send complement to peer
|
||||
List<TradePresenceData> safeTradePresences = List.copyOf(entriesUnknownToPeer.values());
|
||||
Message responseMessage = new TradePresencesMessage(safeTradePresences);
|
||||
if (!peer.sendMessage(responseMessage)) {
|
||||
peer.disconnect("failed to send TRADE_PRESENCES response");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void onTradePresencesMessage(Peer peer, Message message) {
|
||||
TradePresencesMessage tradePresencesMessage = (TradePresencesMessage) message;
|
||||
|
||||
List<TradePresenceData> peersTradePresences = tradePresencesMessage.getTradePresences();
|
||||
|
||||
long now = NTP.getTime();
|
||||
// Timestamps before this are too far into the past
|
||||
long pastThreshold = now;
|
||||
// Timestamps after this are too far into the future
|
||||
long futureThreshold = now + PRESENCE_LIFETIME;
|
||||
|
||||
Map<ByteArray, Supplier<ACCT>> acctSuppliersByCodeHash = SupportedBlockchain.getAcctMap();
|
||||
|
||||
int newCount = 0;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
for (TradePresenceData peersTradePresence : peersTradePresences) {
|
||||
long timestamp = peersTradePresence.getTimestamp();
|
||||
|
||||
// Ignore if timestamp is out of bounds
|
||||
if (timestamp < pastThreshold || timestamp > futureThreshold) {
|
||||
if (timestamp < pastThreshold)
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is too old vs {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, pastThreshold
|
||||
);
|
||||
else
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is too new vs {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, pastThreshold
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ByteArray pubkeyByteArray = ByteArray.wrap(peersTradePresence.getPublicKey());
|
||||
|
||||
// Ignore if we've previously verified this timestamp+publickey combo or sent timestamp is older
|
||||
TradePresenceData existingTradeData = this.safeAllTradePresencesByPubkey.get(pubkeyByteArray);
|
||||
if (existingTradeData != null && timestamp <= existingTradeData.getTimestamp()) {
|
||||
if (timestamp == existingTradeData.getTimestamp())
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as we have verified timestamp {} before",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp
|
||||
);
|
||||
else
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as timestamp {} is older than latest {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp, existingTradeData.getTimestamp()
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check timestamp signature
|
||||
byte[] timestampSignature = peersTradePresence.getSignature();
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
byte[] publicKey = peersTradePresence.getPublicKey();
|
||||
if (!Crypto.verify(publicKey, timestampSignature, timestampBytes)) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as signature failed to verify",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ATData atData = repository.getATRepository().fromATAddress(peersTradePresence.getAtAddress());
|
||||
if (atData == null || atData.getIsFrozen() || atData.getIsFinished()) {
|
||||
if (atData == null)
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT doesn't exist",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
else
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT is frozen or finished",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ByteArray atCodeHash = ByteArray.wrap(atData.getCodeHash());
|
||||
Supplier<ACCT> acctSupplier = acctSuppliersByCodeHash.get(atCodeHash);
|
||||
if (acctSupplier == null) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as AT isn't a known ACCT?",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
CrossChainTradeData tradeData = acctSupplier.get().populateTradeData(repository, atData);
|
||||
if (tradeData == null) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as trade data not found?",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert signer's public key to address form
|
||||
String signerAddress = peersTradePresence.getTradeAddress();
|
||||
|
||||
// Signer's public key (in address form) must match Bob's / Alice's trade public key (in address form)
|
||||
if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress)) {
|
||||
LOGGER.trace("Ignoring trade presence {} from peer {} as signer isn't Alice or Bob?",
|
||||
peersTradePresence.getAtAddress(), peer
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// This is new to us
|
||||
this.allTradePresencesByPubkey.put(pubkeyByteArray, peersTradePresence);
|
||||
++newCount;
|
||||
|
||||
LOGGER.trace("Added trade presence {} from peer {} with timestamp {}",
|
||||
peersTradePresence.getAtAddress(), peer, timestamp
|
||||
);
|
||||
|
||||
EventBus.INSTANCE.notify(new TradePresenceEvent(peersTradePresence));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't process TRADE_PRESENCES message due to repository issue", e);
|
||||
}
|
||||
|
||||
if (newCount > 0) {
|
||||
LOGGER.debug("New trade presences: {}", newCount);
|
||||
rebuildSafeAllTradePresences();
|
||||
}
|
||||
}
|
||||
|
||||
public void bridgePresence(long timestamp, byte[] publicKey, byte[] signature, String atAddress) {
|
||||
long expiry = generateExpiry(timestamp);
|
||||
ByteArray pubkeyByteArray = ByteArray.wrap(publicKey);
|
||||
|
||||
TradePresenceData fakeTradePresenceData = new TradePresenceData(expiry, publicKey, signature, atAddress);
|
||||
|
||||
// Only bridge if trade presence expiry timestamp is newer
|
||||
TradePresenceData computedTradePresenceData = this.allTradePresencesByPubkey.compute(pubkeyByteArray, (k, v) ->
|
||||
v == null || v.getTimestamp() < expiry ? fakeTradePresenceData : v
|
||||
);
|
||||
|
||||
if (computedTradePresenceData == fakeTradePresenceData) {
|
||||
LOGGER.trace("Bridged PRESENCE transaction for trade {} with timestamp {}", atAddress, expiry);
|
||||
rebuildSafeAllTradePresences();
|
||||
|
||||
EventBus.INSTANCE.notify(new TradePresenceEvent(fakeTradePresenceData));
|
||||
}
|
||||
}
|
||||
|
||||
/** Decorates a CrossChainTradeData object with Alice / Bob trade-bot presence timestamp, if available. */
|
||||
public void decorateTradeDataWithPresence(CrossChainTradeData crossChainTradeData) {
|
||||
// Match by AT address, then check for Bob vs Alice
|
||||
this.safeAllTradePresencesByPubkey.values().stream()
|
||||
.filter(tradePresenceData -> tradePresenceData.getAtAddress().equals(crossChainTradeData.qortalAtAddress))
|
||||
.forEach(tradePresenceData -> {
|
||||
String signerAddress = tradePresenceData.getTradeAddress();
|
||||
|
||||
// Signer's public key (in address form) must match Bob's / Alice's trade public key (in address form)
|
||||
if (signerAddress.equals(crossChainTradeData.qortalCreatorTradeAddress))
|
||||
crossChainTradeData.creatorPresenceExpiry = tradePresenceData.getTimestamp();
|
||||
else if (signerAddress.equals(crossChainTradeData.qortalPartnerAddress))
|
||||
crossChainTradeData.partnerPresenceExpiry = tradePresenceData.getTimestamp();
|
||||
});
|
||||
}
|
||||
|
||||
private long generateExpiry(long timestamp) {
|
||||
return ((timestamp - 1) / EXPIRY_ROUNDING) * EXPIRY_ROUNDING + PRESENCE_LIFETIME;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,30 +42,40 @@ public class Bitcoin extends Bitcoiny {
|
||||
public Collection<ElectrumX.Server> getServers() {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002),
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=btc
|
||||
//CLOSED new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002),
|
||||
//CLOSED new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002),
|
||||
//CLOSED new Server("electrum.pabu.io", Server.ConnectionType.SSL, 50002),
|
||||
//CLOSED new Server("electrumx.hodlwallet.com", Server.ConnectionType.SSL, 50002),
|
||||
//CLOSED new Server("gd42.org", Server.ConnectionType.SSL, 50002),
|
||||
//CLOSED new Server("korea.electrum-server.com", Server.ConnectionType.SSL, 50002),
|
||||
//CLOSED new Server("prospero.bitsrc.net", Server.ConnectionType.SSL, 50002),
|
||||
//1.15.0 new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
|
||||
//1.15.0 new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002),
|
||||
//1.14.0 new Server("electrum.coinext.com.br", Server.ConnectionType.SSL, 50002),
|
||||
new Server("104.248.139.211", Server.ConnectionType.SSL, 50002),
|
||||
new Server("142.93.6.38", Server.ConnectionType.SSL, 50002),
|
||||
new Server("157.245.172.236", Server.ConnectionType.SSL, 50002),
|
||||
new Server("167.172.226.175", Server.ConnectionType.SSL, 50002),
|
||||
new Server("167.172.42.31", Server.ConnectionType.SSL, 50002),
|
||||
new Server("178.62.80.20", Server.ConnectionType.SSL, 50002),
|
||||
new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
|
||||
new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002),
|
||||
new Server("xtrum.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002),
|
||||
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.coinext.com.br", Server.ConnectionType.TCP, 50001),
|
||||
new Server("korea.electrum-server.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("eai.coincited.net", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.coinext.com.br", Server.ConnectionType.SSL, 50002),
|
||||
new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("korea.electrum-server.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002),
|
||||
new Server("68.183.188.105", Server.ConnectionType.SSL, 50002),
|
||||
new Server("bitcoin.lukechilds.co", Server.ConnectionType.SSL, 50002),
|
||||
new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.hodlwallet.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("blkhub.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("btce.iiiiiii.biz", Server.ConnectionType.SSL, 50002),
|
||||
new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
|
||||
new Server("eai.coincited.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("prospero.bitsrc.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("gd42.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.pabu.io", Server.ConnectionType.SSL, 50002));
|
||||
new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrumx.dev", Server.ConnectionType.SSL, 50002),
|
||||
new Server("elx.bitske.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("fortress.qtornado.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("guichet.centure.cc", Server.ConnectionType.SSL, 50002),
|
||||
new Server("kareoke.qoppa.org", Server.ConnectionType.SSL, 50002),
|
||||
new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
|
||||
new Server("node1.btccuracao.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("xtrum.com", Server.ConnectionType.SSL, 50002));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@@ -39,6 +33,7 @@ import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
/** Bitcoin-like (Bitcoin, Litecoin, etc.) support */
|
||||
public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
@@ -53,13 +48,24 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
protected final NetworkParameters params;
|
||||
|
||||
/** Cache recent transactions to speed up subsequent lookups */
|
||||
protected List<SimpleTransaction> transactionsCache;
|
||||
protected Long transactionsCacheTimestamp;
|
||||
protected String transactionsCacheXpub;
|
||||
protected static long TRANSACTIONS_CACHE_TIMEOUT = 2 * 60 * 1000L; // 2 minutes
|
||||
|
||||
/** Keys that have been previously marked as fully spent,<br>
|
||||
* i.e. keys with transactions but with no unspent outputs. */
|
||||
protected final Set<ECKey> spentKeys = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
/** How many bitcoinj wallet keys to generate in each batch. */
|
||||
/** How many wallet keys to generate in each batch. */
|
||||
private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3;
|
||||
|
||||
/** 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;
|
||||
|
||||
@@ -98,8 +104,9 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
try {
|
||||
ScriptType addressType = Address.fromString(this.params, address).getOutputScriptType();
|
||||
|
||||
return addressType == ScriptType.P2PKH || addressType == ScriptType.P2SH;
|
||||
return addressType == ScriptType.P2PKH || addressType == ScriptType.P2SH || addressType == ScriptType.P2WPKH;
|
||||
} catch (AddressFormatException e) {
|
||||
LOGGER.error(String.format("Unrecognised address format: %s", address));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -228,6 +235,25 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
return transaction.getOutputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns transactions for passed script
|
||||
* <p>
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
public List<TransactionHash> getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
int retries = 0;
|
||||
ForeignBlockchainException e2 = null;
|
||||
while (retries <= 3) {
|
||||
try {
|
||||
return this.blockchain.getAddressTransactions(scriptPubKey, includeUnconfirmed);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
e2 = e;
|
||||
retries++;
|
||||
}
|
||||
}
|
||||
throw(e2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of transaction hashes pertaining to passed address.
|
||||
* <p>
|
||||
@@ -262,7 +288,17 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* @throws ForeignBlockchainException if error occurs
|
||||
*/
|
||||
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
|
||||
return this.blockchain.getTransaction(txHash);
|
||||
int retries = 0;
|
||||
ForeignBlockchainException e2 = null;
|
||||
while (retries <= 3) {
|
||||
try {
|
||||
return this.blockchain.getTransaction(txHash);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
e2 = e;
|
||||
retries++;
|
||||
}
|
||||
}
|
||||
throw(e2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,70 +373,99 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
return balance.value;
|
||||
}
|
||||
|
||||
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
||||
long balance = 0;
|
||||
Comparator<SimpleTransaction> oldestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp);
|
||||
List<SimpleTransaction> transactions = getWalletTransactions(key58).stream().sorted(oldestTimestampFirstComparator).collect(Collectors.toList());
|
||||
for (SimpleTransaction transaction : transactions) {
|
||||
balance += transaction.getTotalAmount();
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
public List<SimpleTransaction> getWalletTransactions(String key58) throws ForeignBlockchainException {
|
||||
Context.propagate(bitcoinjContext);
|
||||
|
||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||
|
||||
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
|
||||
keyChain.maybeLookAhead();
|
||||
|
||||
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
||||
|
||||
Set<BitcoinyTransaction> walletTransactions = new HashSet<>();
|
||||
Set<String> keySet = new HashSet<>();
|
||||
|
||||
// Set the number of consecutive empty batches required before giving up
|
||||
final int numberOfAdditionalBatchesToSearch = 5;
|
||||
|
||||
int unusedCounter = 0;
|
||||
int ki = 0;
|
||||
do {
|
||||
boolean areAllKeysUnused = true;
|
||||
|
||||
for (; ki < keys.size(); ++ki) {
|
||||
DeterministicKey dKey = keys.get(ki);
|
||||
|
||||
// Check for transactions
|
||||
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||
keySet.add(address.toString());
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<TransactionHash> historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
areAllKeysUnused = false;
|
||||
|
||||
for (TransactionHash transactionHash : historicTransactionHashes)
|
||||
walletTransactions.add(this.getTransaction(transactionHash.txHash));
|
||||
synchronized (this) {
|
||||
// Serve from the cache if it's recent, and matches this xpub
|
||||
if (Objects.equals(transactionsCacheXpub, key58)) {
|
||||
if (transactionsCache != null && transactionsCacheTimestamp != null) {
|
||||
Long now = NTP.getTime();
|
||||
boolean isCacheStale = (now != null && now - transactionsCacheTimestamp >= TRANSACTIONS_CACHE_TIMEOUT);
|
||||
if (!isCacheStale) {
|
||||
return transactionsCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (areAllKeysUnused) {
|
||||
// No transactions
|
||||
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
|
||||
// ... and we've hit our search limit
|
||||
break;
|
||||
Context.propagate(bitcoinjContext);
|
||||
|
||||
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||
|
||||
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
|
||||
keyChain.maybeLookAhead();
|
||||
|
||||
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
||||
|
||||
Set<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 {
|
||||
boolean areAllKeysUnused = true;
|
||||
|
||||
for (; ki < keys.size(); ++ki) {
|
||||
DeterministicKey dKey = keys.get(ki);
|
||||
|
||||
// Check for transactions
|
||||
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||
keySet.add(address.toString());
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
areAllKeysUnused = false;
|
||||
|
||||
for (TransactionHash transactionHash : historicTransactionHashes)
|
||||
walletTransactions.add(this.getTransaction(transactionHash.txHash));
|
||||
}
|
||||
}
|
||||
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||
unusedCounter++;
|
||||
}
|
||||
else {
|
||||
// Some keys in this batch were used, so reset the counter
|
||||
unusedCounter = 0;
|
||||
}
|
||||
|
||||
// Generate some more keys
|
||||
keys.addAll(generateMoreKeys(keyChain));
|
||||
if (areAllKeysUnused) {
|
||||
// No transactions
|
||||
if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
|
||||
// ... and we've hit our search limit
|
||||
break;
|
||||
}
|
||||
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||
unusedCounter++;
|
||||
} else {
|
||||
// Some keys in this batch were used, so reset the counter
|
||||
unusedCounter = 0;
|
||||
}
|
||||
|
||||
// Process new keys
|
||||
} while (true);
|
||||
// Generate some more keys
|
||||
keys.addAll(generateMoreKeys(keyChain));
|
||||
|
||||
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
|
||||
// Process new keys
|
||||
} while (true);
|
||||
|
||||
return walletTransactions.stream().map(t -> convertToSimpleTransaction(t, keySet)).sorted(newestTimestampFirstComparator).collect(Collectors.toList());
|
||||
Comparator<SimpleTransaction> newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
|
||||
|
||||
// Update cache and return
|
||||
transactionsCacheTimestamp = NTP.getTime();
|
||||
transactionsCacheXpub = key58;
|
||||
transactionsCache = walletTransactions.stream()
|
||||
.map(t -> convertToSimpleTransaction(t, keySet))
|
||||
.sorted(newestTimestampFirstComparator).collect(Collectors.toList());
|
||||
|
||||
return transactionsCache;
|
||||
}
|
||||
}
|
||||
|
||||
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
||||
@@ -411,19 +476,27 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
List<SimpleTransaction.Input> inputs = new ArrayList<>();
|
||||
List<SimpleTransaction.Output> outputs = new ArrayList<>();
|
||||
|
||||
boolean anyOutputAddressInWallet = false;
|
||||
boolean transactionInvolvesExternalWallet = false;
|
||||
|
||||
for (BitcoinyTransaction.Input input : t.inputs) {
|
||||
try {
|
||||
BitcoinyTransaction t2 = getTransaction(input.outputTxHash);
|
||||
List<String> senders = t2.outputs.get(input.outputVout).addresses;
|
||||
long inputAmount = t2.outputs.get(input.outputVout).value;
|
||||
totalInputAmount += inputAmount;
|
||||
for (String sender : senders) {
|
||||
boolean addressInWallet = false;
|
||||
if (keySet.contains(sender)) {
|
||||
total += inputAmount;
|
||||
addressInWallet = true;
|
||||
if (senders != null) {
|
||||
for (String sender : senders) {
|
||||
boolean addressInWallet = false;
|
||||
if (keySet.contains(sender)) {
|
||||
total += inputAmount;
|
||||
addressInWallet = true;
|
||||
}
|
||||
else {
|
||||
transactionInvolvesExternalWallet = true;
|
||||
}
|
||||
inputs.add(new SimpleTransaction.Input(sender, inputAmount, addressInWallet));
|
||||
}
|
||||
inputs.add(new SimpleTransaction.Input(sender, inputAmount, addressInWallet));
|
||||
}
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.trace("Failed to retrieve transaction information {}", input.outputTxHash);
|
||||
@@ -431,22 +504,39 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
}
|
||||
if (t.outputs != null && !t.outputs.isEmpty()) {
|
||||
for (BitcoinyTransaction.Output output : t.outputs) {
|
||||
for (String address : output.addresses) {
|
||||
boolean addressInWallet = false;
|
||||
if (keySet.contains(address)) {
|
||||
if (total > 0L) {
|
||||
amount -= (total - output.value);
|
||||
} else {
|
||||
amount += output.value;
|
||||
if (output.addresses != null) {
|
||||
for (String address : output.addresses) {
|
||||
boolean addressInWallet = false;
|
||||
if (keySet.contains(address)) {
|
||||
if (total > 0L) { // Change returned from sent amount
|
||||
amount -= (total - output.value);
|
||||
} else { // Amount received
|
||||
amount += output.value;
|
||||
}
|
||||
addressInWallet = true;
|
||||
anyOutputAddressInWallet = true;
|
||||
}
|
||||
addressInWallet = true;
|
||||
else {
|
||||
transactionInvolvesExternalWallet = true;
|
||||
}
|
||||
outputs.add(new SimpleTransaction.Output(address, output.value, addressInWallet));
|
||||
}
|
||||
outputs.add(new SimpleTransaction.Output(address, output.value, addressInWallet));
|
||||
}
|
||||
totalOutputAmount += output.value;
|
||||
}
|
||||
}
|
||||
long fee = totalInputAmount - totalOutputAmount;
|
||||
|
||||
if (!anyOutputAddressInWallet) {
|
||||
// No outputs relate to this wallet - check if any inputs did (which is signified by a positive total)
|
||||
if (total > 0) {
|
||||
amount = total * -1;
|
||||
}
|
||||
}
|
||||
else if (!transactionInvolvesExternalWallet) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -539,7 +629,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);
|
||||
this.keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT_BITCOINJ);
|
||||
this.keyChain.maybeLookAhead();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,13 @@ public class Dogecoin extends Bitcoiny {
|
||||
|
||||
public static final String CURRENCY_CODE = "DOGE";
|
||||
|
||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(500000000); // 5 DOGE per 1000 bytes
|
||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(1000000); // 0.01 DOGE per 1000 bytes
|
||||
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 300000000L; // 3 DOGE minimum order. The RPC dust threshold is around 2 DOGE
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 100000000L; // 1 DOGE minimum order. See recommendations:
|
||||
// https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long MAINNET_FEE = 110000000L;
|
||||
private static final long MAINNET_FEE = 100000L;
|
||||
private static final long NON_MAINNET_FEE = 10000L; // TODO: calibrate this
|
||||
|
||||
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
|
||||
@@ -44,9 +45,9 @@ public class Dogecoin extends Bitcoiny {
|
||||
public Collection<Server> getServers() {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
new Server("electrum1.cipig.net", ConnectionType.TCP, 10060),
|
||||
new Server("electrum2.cipig.net", ConnectionType.TCP, 10060),
|
||||
new Server("electrum3.cipig.net", ConnectionType.TCP, 10060));
|
||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20060),
|
||||
new Server("electrum2.cipig.net", ConnectionType.SSL, 20060),
|
||||
new Server("electrum3.cipig.net", ConnectionType.SSL, 20060));
|
||||
// TODO: add more mainnet servers. It's too centralized.
|
||||
}
|
||||
|
||||
|
||||
@@ -5,19 +5,7 @@ import java.math.BigDecimal;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Random;
|
||||
import java.util.Scanner;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -50,6 +38,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
/** Error message sent by some ElectrumX servers when they don't support returning verbose transactions. */
|
||||
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
||||
|
||||
private static final int RESPONSE_TIME_READINGS = 5;
|
||||
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
|
||||
|
||||
public static class Server {
|
||||
String hostname;
|
||||
|
||||
@@ -57,6 +48,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
ConnectionType connectionType;
|
||||
|
||||
int port;
|
||||
private List<Long> responseTimes = new ArrayList<>();
|
||||
|
||||
public Server(String hostname, ConnectionType connectionType, int port) {
|
||||
this.hostname = hostname;
|
||||
@@ -64,6 +56,25 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
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)
|
||||
@@ -103,7 +114,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
private Scanner scanner;
|
||||
private int nextId = 1;
|
||||
|
||||
private static final int TX_CACHE_SIZE = 200;
|
||||
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
|
||||
@@ -390,13 +401,36 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
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
|
||||
// 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)
|
||||
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));
|
||||
@@ -539,6 +573,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
|
||||
while (haveConnection()) {
|
||||
Object response = connectedRpc(method, params);
|
||||
|
||||
// If we have more servers and this 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();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (response != null)
|
||||
return response;
|
||||
|
||||
@@ -628,6 +673,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
String request = requestJson.toJSONString() + "\n";
|
||||
LOGGER.trace(() -> String.format("Request: %s", request));
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
final String response;
|
||||
|
||||
try {
|
||||
@@ -638,7 +684,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
long responseTime = endTime-startTime;
|
||||
|
||||
LOGGER.trace(() -> String.format("Response: %s", response));
|
||||
LOGGER.trace(() -> String.format("Time taken: %dms", endTime-startTime));
|
||||
|
||||
if (response.isEmpty())
|
||||
// Empty response - try another server?
|
||||
@@ -649,6 +699,11 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
||||
// Unexpected response - try another server?
|
||||
return null;
|
||||
|
||||
// Keep track of response times
|
||||
if (this.currentServer != null) {
|
||||
this.currentServer.addResponseTime(responseTime);
|
||||
}
|
||||
|
||||
JSONObject responseJson = (JSONObject) responseObj;
|
||||
|
||||
Object errorObj = responseJson.get("error");
|
||||
|
||||
@@ -44,19 +44,17 @@ public class Litecoin extends Bitcoiny {
|
||||
public Collection<ElectrumX.Server> getServers() {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
new Server("electrum-ltc.someguy123.net", Server.ConnectionType.SSL, 50002),
|
||||
new Server("backup.electrum-ltc.org", Server.ConnectionType.TCP, 50001),
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=ltc
|
||||
//CLOSED new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
||||
//CLOSED new Server("electrum-ltc.someguy123.net", Server.ConnectionType.SSL, 50002),
|
||||
//PHISHY new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002),
|
||||
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443),
|
||||
new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002),
|
||||
new Server("ltc.rentonisk.com", Server.ConnectionType.TCP, 50001),
|
||||
new Server("ltc.rentonisk.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
||||
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
|
||||
new Server("electrum-ltc-bysh.me", Server.ConnectionType.TCP, 50002),
|
||||
new Server("electrum.jochen-hoenicke.de", Server.ConnectionType.TCP, 50005),
|
||||
new Server("node.ispol.sk", Server.ConnectionType.TCP, 50004));
|
||||
new Server("ltc.rentonrisk.com", Server.ConnectionType.SSL, 50002));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -62,7 +62,7 @@ public enum SupportedBlockchain {
|
||||
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
|
||||
.map(supportedBlockchain -> supportedBlockchain.supportedAccts)
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.toUnmodifiableMap(triple -> new ByteArray(triple.getB()), Triple::getC));
|
||||
.collect(Collectors.toUnmodifiableMap(triple -> ByteArray.wrap(triple.getB()), Triple::getC));
|
||||
|
||||
private static final Map<String, Supplier<ACCT>> supportedAcctsByName = Arrays.stream(SupportedBlockchain.values())
|
||||
.map(supportedBlockchain -> supportedBlockchain.supportedAccts)
|
||||
@@ -94,7 +94,7 @@ public enum SupportedBlockchain {
|
||||
return getAcctMap();
|
||||
|
||||
return blockchain.supportedAccts.stream()
|
||||
.collect(Collectors.toUnmodifiableMap(triple -> new ByteArray(triple.getB()), Triple::getC));
|
||||
.collect(Collectors.toUnmodifiableMap(triple -> ByteArray.wrap(triple.getB()), Triple::getC));
|
||||
}
|
||||
|
||||
public static Map<ByteArray, Supplier<ACCT>> getFilteredAcctMap(String specificBlockchain) {
|
||||
@@ -109,7 +109,7 @@ public enum SupportedBlockchain {
|
||||
}
|
||||
|
||||
public static ACCT getAcctByCodeHash(byte[] codeHash) {
|
||||
ByteArray wrappedCodeHash = new ByteArray(codeHash);
|
||||
ByteArray wrappedCodeHash = ByteArray.wrap(codeHash);
|
||||
|
||||
Supplier<ACCT> acctInstanceSupplier = supportedAcctsByCodeHash.get(wrappedCodeHash);
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.qortal.data.arbitrary;
|
||||
|
||||
public class ArbitraryCategoryInfo {
|
||||
|
||||
public String id;
|
||||
public String name;
|
||||
|
||||
public ArbitraryCategoryInfo() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.qortal.data.arbitrary;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ArbitraryDirectConnectionInfo {
|
||||
|
||||
private final byte[] signature;
|
||||
private final String peerAddress;
|
||||
private final List<byte[]> hashes;
|
||||
private final long timestamp;
|
||||
|
||||
public ArbitraryDirectConnectionInfo(byte[] signature, String peerAddress, List<byte[]> hashes, long timestamp) {
|
||||
this.signature = signature;
|
||||
this.peerAddress = peerAddress;
|
||||
this.hashes = hashes;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public String getPeerAddress() {
|
||||
return this.peerAddress;
|
||||
}
|
||||
|
||||
public List<byte[]> getHashes() {
|
||||
return this.hashes;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public int getHashCount() {
|
||||
if (this.hashes == null) {
|
||||
return 0;
|
||||
}
|
||||
return this.hashes.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == this)
|
||||
return true;
|
||||
|
||||
if (!(other instanceof ArbitraryDirectConnectionInfo))
|
||||
return false;
|
||||
|
||||
ArbitraryDirectConnectionInfo otherDirectConnectionInfo = (ArbitraryDirectConnectionInfo) other;
|
||||
|
||||
return Arrays.equals(this.signature, otherDirectConnectionInfo.getSignature())
|
||||
&& Objects.equals(this.peerAddress, otherDirectConnectionInfo.getPeerAddress())
|
||||
&& Objects.equals(this.hashes, otherDirectConnectionInfo.getHashes())
|
||||
&& Objects.equals(this.timestamp, otherDirectConnectionInfo.getTimestamp());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.qortal.data.arbitrary;
|
||||
|
||||
import org.qortal.network.Peer;
|
||||
|
||||
public class ArbitraryFileListResponseInfo extends ArbitraryRelayInfo {
|
||||
|
||||
public ArbitraryFileListResponseInfo(String hash58, String signature58, Peer peer, Long timestamp, Long requestTime, Integer requestHops) {
|
||||
super(hash58, signature58, peer, timestamp, requestTime, requestHops);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.qortal.data.arbitrary;
|
||||
|
||||
import org.qortal.network.Peer;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ArbitraryRelayInfo {
|
||||
|
||||
private final String hash58;
|
||||
private final String signature58;
|
||||
private final Peer peer;
|
||||
private final Long timestamp;
|
||||
private final Long requestTime;
|
||||
private final Integer requestHops;
|
||||
|
||||
public ArbitraryRelayInfo(String hash58, String signature58, Peer peer, Long timestamp, Long requestTime, Integer requestHops) {
|
||||
this.hash58 = hash58;
|
||||
this.signature58 = signature58;
|
||||
this.peer = peer;
|
||||
this.timestamp = timestamp;
|
||||
this.requestTime = requestTime;
|
||||
this.requestHops = requestHops;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return this.getHash58() != null && this.getSignature58() != null
|
||||
&& this.getPeer() != null && this.getTimestamp() != null;
|
||||
}
|
||||
|
||||
public String getHash58() {
|
||||
return this.hash58;
|
||||
}
|
||||
|
||||
public String getSignature58() {
|
||||
return signature58;
|
||||
}
|
||||
|
||||
public Peer getPeer() {
|
||||
return peer;
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
public Integer getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s = %s, %s, %d", this.hash58, this.signature58, this.peer, this.timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == this)
|
||||
return true;
|
||||
|
||||
if (!(other instanceof ArbitraryRelayInfo))
|
||||
return false;
|
||||
|
||||
ArbitraryRelayInfo otherRelayInfo = (ArbitraryRelayInfo) other;
|
||||
|
||||
return this.peer == otherRelayInfo.getPeer()
|
||||
&& Objects.equals(this.hash58, otherRelayInfo.getHash58())
|
||||
&& Objects.equals(this.signature58, otherRelayInfo.getSignature58());
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public class ArbitraryResourceInfo {
|
||||
public Service service;
|
||||
public String identifier;
|
||||
public ArbitraryResourceStatus status;
|
||||
public ArbitraryResourceMetadata metadata;
|
||||
|
||||
public Long size;
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.qortal.data.arbitrary;
|
||||
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ArbitraryResourceMetadata {
|
||||
|
||||
private String title;
|
||||
private String description;
|
||||
private List<String> tags;
|
||||
private Category category;
|
||||
private String categoryName;
|
||||
|
||||
public ArbitraryResourceMetadata() {
|
||||
}
|
||||
|
||||
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.tags = tags;
|
||||
this.category = category;
|
||||
this.categoryName = category.getName();
|
||||
}
|
||||
|
||||
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata) {
|
||||
if (transactionMetadata == null) {
|
||||
return null;
|
||||
}
|
||||
String title = transactionMetadata.getTitle();
|
||||
String description = transactionMetadata.getDescription();
|
||||
List<String> tags = transactionMetadata.getTags();
|
||||
Category category = transactionMetadata.getCategory();
|
||||
|
||||
if (title == null && description == null && tags == null && category == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ArbitraryResourceMetadata(title, description, tags, category);
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,21 @@ public class ArbitraryResourceStatus {
|
||||
private String title;
|
||||
private String description;
|
||||
|
||||
private Integer localChunkCount;
|
||||
private Integer totalChunkCount;
|
||||
|
||||
public ArbitraryResourceStatus() {
|
||||
}
|
||||
|
||||
public ArbitraryResourceStatus(Status status) {
|
||||
public ArbitraryResourceStatus(Status status, Integer localChunkCount, Integer totalChunkCount) {
|
||||
this.id = status.toString();
|
||||
this.title = status.title;
|
||||
this.description = status.description;
|
||||
this.localChunkCount = localChunkCount;
|
||||
this.totalChunkCount = totalChunkCount;
|
||||
}
|
||||
|
||||
public ArbitraryResourceStatus(Status status) {
|
||||
this(status, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,12 @@ public class CrossChainTradeData {
|
||||
|
||||
public String acctName;
|
||||
|
||||
@Schema(description = "Timestamp when AT creator's trade-bot presence expires")
|
||||
public Long creatorPresenceExpiry;
|
||||
|
||||
@Schema(description = "Timestamp when trade partner's trade-bot presence expires")
|
||||
public Long partnerPresenceExpiry;
|
||||
|
||||
// Constructors
|
||||
|
||||
// Necessary for JAXB
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GroupData {
|
||||
private ApprovalThreshold approvalThreshold;
|
||||
private int minimumBlockDelay;
|
||||
private int maximumBlockDelay;
|
||||
public int memberCount;
|
||||
|
||||
/** Reference to CREATE_GROUP or UPDATE_GROUP transaction, used to rebuild group during orphaning. */
|
||||
// No need to ever expose this via API
|
||||
|
||||
114
src/main/java/org/qortal/data/network/TradePresenceData.java
Normal file
114
src/main/java/org/qortal/data/network/TradePresenceData.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package org.qortal.data.network;
|
||||
|
||||
import org.qortal.crypto.Crypto;
|
||||
|
||||
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 javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import java.util.Arrays;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class TradePresenceData {
|
||||
|
||||
protected long timestamp;
|
||||
|
||||
@XmlJavaTypeAdapter(
|
||||
type = byte[].class,
|
||||
value = org.qortal.api.Base58TypeAdapter.class
|
||||
)
|
||||
protected byte[] publicKey; // Could be BOB's or ALICE's
|
||||
|
||||
// No need to send this via websocket / API
|
||||
@XmlTransient
|
||||
protected byte[] signature; // Not always present
|
||||
|
||||
protected String atAddress; // Not always present
|
||||
|
||||
// Have JAXB use getter instead
|
||||
@XmlTransient
|
||||
protected String tradeAddress; // Lazily instantiated
|
||||
|
||||
// Constructors
|
||||
|
||||
// necessary for JAXB serialization
|
||||
protected TradePresenceData() {
|
||||
}
|
||||
|
||||
public TradePresenceData(long timestamp, byte[] publicKey, byte[] signature, String atAddress) {
|
||||
this.timestamp = timestamp;
|
||||
this.publicKey = publicKey;
|
||||
this.signature = signature;
|
||||
this.atAddress = atAddress;
|
||||
}
|
||||
|
||||
public TradePresenceData(long timestamp, byte[] publicKey) {
|
||||
this(timestamp, publicKey, null, null);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public String getAtAddress() {
|
||||
return this.atAddress;
|
||||
}
|
||||
|
||||
// Probably doesn't need synchronization
|
||||
@XmlElement
|
||||
public String getTradeAddress() {
|
||||
if (tradeAddress != null)
|
||||
return tradeAddress;
|
||||
|
||||
tradeAddress = Crypto.toAddress(this.publicKey);
|
||||
return tradeAddress;
|
||||
}
|
||||
|
||||
// Comparison
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == this)
|
||||
return true;
|
||||
|
||||
if (!(other instanceof TradePresenceData))
|
||||
return false;
|
||||
|
||||
TradePresenceData otherTradePresenceData = (TradePresenceData) other;
|
||||
|
||||
// Very quick comparison
|
||||
if (otherTradePresenceData.timestamp != this.timestamp)
|
||||
return false;
|
||||
|
||||
if (!Arrays.equals(otherTradePresenceData.publicKey, this.publicKey))
|
||||
return false;
|
||||
|
||||
if (otherTradePresenceData.atAddress != null && !otherTradePresenceData.atAddress.equals(this.atAddress))
|
||||
return false;
|
||||
|
||||
if (this.atAddress != null && !this.atAddress.equals(otherTradePresenceData.atAddress))
|
||||
return false;
|
||||
|
||||
if (!Arrays.equals(otherTradePresenceData.signature, this.signature))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Pretty lazy implementation
|
||||
return (int) this.timestamp;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,6 +48,7 @@ public class UpdateNameTransactionData extends TransactionData {
|
||||
|
||||
public void afterUnmarshal(Unmarshaller u, Object parent) {
|
||||
this.creatorPublicKey = this.ownerPublicKey;
|
||||
this.reducedNewName = this.newName != null ? Unicode.sanitize(this.newName) : null;
|
||||
}
|
||||
|
||||
/** From repository */
|
||||
@@ -62,7 +63,7 @@ public class UpdateNameTransactionData extends TransactionData {
|
||||
this.nameReference = nameReference;
|
||||
}
|
||||
|
||||
/** From network/API */
|
||||
/** From network */
|
||||
public UpdateNameTransactionData(BaseTransactionData baseTransactionData, String name, String newName, String newData) {
|
||||
this(baseTransactionData, name, newName, newData, Unicode.sanitize(newName), null);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.awt.GraphicsEnvironment;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ServiceConfigurationError;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JOptionPane;
|
||||
@@ -49,7 +50,7 @@ public class Gui {
|
||||
protected static BufferedImage loadImage(String resourceName) {
|
||||
try (InputStream in = Gui.class.getResourceAsStream("/images/" + resourceName)) {
|
||||
return ImageIO.read(in);
|
||||
} catch (IllegalArgumentException | IOException e) {
|
||||
} catch (IllegalArgumentException | IOException | ServiceConfigurationError e) {
|
||||
LOGGER.warn(String.format("Couldn't locate image resource \"images/%s\"", resourceName));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.network.message.ChallengeMessage;
|
||||
import org.qortal.network.message.HelloMessage;
|
||||
import org.qortal.network.message.Message;
|
||||
import org.qortal.network.message.Message.MessageType;
|
||||
import org.qortal.network.message.MessageType;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.network.message.ResponseMessage;
|
||||
import org.qortal.utils.DaemonThreadFactory;
|
||||
@@ -74,6 +74,12 @@ public enum Handshake {
|
||||
peer.setPeersConnectionTimestamp(peersConnectionTimestamp);
|
||||
peer.setPeersVersion(versionString, version);
|
||||
|
||||
// Ensure the peer is running at least the version specified in MIN_PEER_VERSION
|
||||
if (peer.isAtLeastVersion(MIN_PEER_VERSION) == false) {
|
||||
LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Settings.getInstance().getAllowConnectionsWithOlderPeerVersions() == false) {
|
||||
// Ensure the peer is running at least the minimum version allowed for connections
|
||||
final String minPeerVersion = Settings.getInstance().getMinPeerVersion();
|
||||
@@ -258,6 +264,9 @@ public enum Handshake {
|
||||
|
||||
private static final long PEER_VERSION_131 = 0x0100030001L;
|
||||
|
||||
/** Minimum peer version that we are allowed to communicate with */
|
||||
private static final String MIN_PEER_VERSION = "3.1.0";
|
||||
|
||||
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
||||
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
||||
// Can always be made harder in the future...
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
package org.qortal.network;
|
||||
|
||||
import com.dosse.upnp.UPnP;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataFileManager;
|
||||
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.network.PeerData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.network.message.*;
|
||||
import org.qortal.network.task.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
@@ -32,6 +34,7 @@ import java.nio.channels.*;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
@@ -41,9 +44,8 @@ import java.util.stream.Collectors;
|
||||
// For managing peers
|
||||
public class Network {
|
||||
private static final Logger LOGGER = LogManager.getLogger(Network.class);
|
||||
private static Network instance;
|
||||
|
||||
private static final int LISTEN_BACKLOG = 10;
|
||||
private static final int LISTEN_BACKLOG = 5;
|
||||
/**
|
||||
* How long before retrying after a connection failure, in milliseconds.
|
||||
*/
|
||||
@@ -100,25 +102,38 @@ public class Network {
|
||||
private long nextDisconnectionCheck = 0L;
|
||||
|
||||
private final List<PeerData> allKnownPeers = new ArrayList<>();
|
||||
private final List<Peer> connectedPeers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Maintain two lists for each subset of peers:
|
||||
* - A synchronizedList, to be modified when peers are added/removed
|
||||
* - An immutable List, which is rebuilt automatically to mirror the synchronized list, and is then served to consumers
|
||||
* This allows for thread safety without having to synchronize every time a thread requests a peer list
|
||||
*/
|
||||
private final List<Peer> connectedPeers = Collections.synchronizedList(new ArrayList<>());
|
||||
private List<Peer> immutableConnectedPeers = Collections.emptyList(); // always rebuilt from mutable, synced list above
|
||||
|
||||
private final List<Peer> handshakedPeers = Collections.synchronizedList(new ArrayList<>());
|
||||
private List<Peer> immutableHandshakedPeers = Collections.emptyList(); // always rebuilt from mutable, synced list above
|
||||
|
||||
private final List<Peer> outboundHandshakedPeers = Collections.synchronizedList(new ArrayList<>());
|
||||
private List<Peer> immutableOutboundHandshakedPeers = Collections.emptyList(); // always rebuilt from mutable, synced list above
|
||||
|
||||
|
||||
private final List<PeerAddress> selfPeers = new ArrayList<>();
|
||||
|
||||
private final ExecuteProduceConsume networkEPC;
|
||||
private Selector channelSelector;
|
||||
private ServerSocketChannel serverChannel;
|
||||
private Iterator<SelectionKey> channelIterator = null;
|
||||
|
||||
// volatile because value is updated inside any one of the EPC threads
|
||||
private volatile long nextConnectTaskTimestamp = 0L; // ms - try first connect once NTP syncs
|
||||
|
||||
private final ExecutorService broadcastExecutor = Executors.newCachedThreadPool();
|
||||
// volatile because value is updated inside any one of the EPC threads
|
||||
private volatile long nextBroadcastTimestamp = 0L; // ms - try first broadcast once NTP syncs
|
||||
private SelectionKey serverSelectionKey;
|
||||
private final Set<SelectableChannel> channelsPendingWrite = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final Lock mergePeersLock = new ReentrantLock();
|
||||
|
||||
private List<String> ourExternalIpAddressHistory = new ArrayList<>();
|
||||
private String ourExternalIpAddress = null;
|
||||
private int ourExternalPort = Settings.getInstance().getListenPort();
|
||||
|
||||
private volatile boolean isShuttingDown = false;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -153,7 +168,7 @@ public class Network {
|
||||
serverChannel.configureBlocking(false);
|
||||
serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
|
||||
serverChannel.bind(endpoint, LISTEN_BACKLOG);
|
||||
serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
|
||||
serverSelectionKey = serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
|
||||
} catch (UnknownHostException e) {
|
||||
LOGGER.error("Can't bind listen socket to address {}", Settings.getInstance().getBindAddress());
|
||||
throw new IOException("Can't bind listen socket to address", e);
|
||||
@@ -163,7 +178,8 @@ public class Network {
|
||||
}
|
||||
|
||||
// Load all known peers from repository
|
||||
synchronized (this.allKnownPeers) { List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
|
||||
synchronized (this.allKnownPeers) {
|
||||
List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
|
||||
if (fixedNetwork != null && !fixedNetwork.isEmpty()) {
|
||||
Long addedWhen = NTP.getTime();
|
||||
String addedBy = "fixedNetwork";
|
||||
@@ -183,18 +199,30 @@ public class Network {
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to set up UPnP. All errors are ignored.
|
||||
if (Settings.getInstance().isUPnPEnabled()) {
|
||||
UPnP.openPortTCP(Settings.getInstance().getListenPort());
|
||||
}
|
||||
else {
|
||||
UPnP.closePortTCP(Settings.getInstance().getListenPort());
|
||||
}
|
||||
|
||||
// Start up first networking thread
|
||||
networkEPC.start();
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
|
||||
public static synchronized Network getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new Network();
|
||||
}
|
||||
private static class SingletonContainer {
|
||||
private static final Network INSTANCE = new Network();
|
||||
}
|
||||
|
||||
return instance;
|
||||
public static Network getInstance() {
|
||||
return SingletonContainer.INSTANCE;
|
||||
}
|
||||
|
||||
public int getMaxPeers() {
|
||||
return this.maxPeers;
|
||||
}
|
||||
|
||||
public byte[] getMessageMagic() {
|
||||
@@ -228,10 +256,33 @@ public class Network {
|
||||
}
|
||||
}
|
||||
|
||||
public List<Peer> getConnectedPeers() {
|
||||
synchronized (this.connectedPeers) {
|
||||
return new ArrayList<>(this.connectedPeers);
|
||||
}
|
||||
public List<Peer> getImmutableConnectedPeers() {
|
||||
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)
|
||||
}
|
||||
|
||||
public void removeConnectedPeer(Peer peer) {
|
||||
// Firstly remove from handshaked peers
|
||||
this.removeHandshakedPeer(peer);
|
||||
|
||||
this.connectedPeers.remove(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)
|
||||
}
|
||||
|
||||
public List<PeerAddress> getSelfPeers() {
|
||||
@@ -243,12 +294,15 @@ public class Network {
|
||||
public boolean requestDataFromPeer(String peerAddressString, byte[] signature) {
|
||||
if (peerAddressString != null) {
|
||||
PeerAddress peerAddress = PeerAddress.fromString(peerAddressString);
|
||||
PeerData peerData = null;
|
||||
|
||||
// Reuse an existing PeerData instance if it's already in the known peers list
|
||||
PeerData peerData = this.allKnownPeers.stream()
|
||||
.filter(knownPeerData -> knownPeerData.getAddress().equals(peerAddress))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
synchronized (this.allKnownPeers) {
|
||||
peerData = this.allKnownPeers.stream()
|
||||
.filter(knownPeerData -> knownPeerData.getAddress().equals(peerAddress))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
if (peerData == null) {
|
||||
// Not a known peer, so we need to create one
|
||||
@@ -263,13 +317,14 @@ public class Network {
|
||||
}
|
||||
|
||||
// Check if we're already connected to and handshaked with this peer
|
||||
Peer connectedPeer = this.connectedPeers.stream()
|
||||
.filter(p -> p.getPeerData().getAddress().equals(peerAddress))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
Peer connectedPeer = this.getImmutableConnectedPeers().stream()
|
||||
.filter(p -> p.getPeerData().getAddress().equals(peerAddress))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
boolean isConnected = (connectedPeer != null);
|
||||
|
||||
boolean isHandshaked = this.getHandshakedPeers().stream()
|
||||
boolean isHandshaked = this.getImmutableHandshakedPeers().stream()
|
||||
.anyMatch(p -> p.getPeerData().getAddress().equals(peerAddress));
|
||||
|
||||
if (isConnected && isHandshaked) {
|
||||
@@ -283,6 +338,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
|
||||
@@ -307,46 +363,67 @@ public class Network {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return ArbitraryDataFileManager.getInstance().fetchAllArbitraryDataFiles(repository, connectedPeer, signature);
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to fetch arbitrary data files");
|
||||
}
|
||||
return false;
|
||||
return ArbitraryDataFileListManager.getInstance().fetchArbitraryDataFileList(connectedPeer, signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of connected peers that have completed handshaking.
|
||||
*/
|
||||
public List<Peer> getHandshakedPeers() {
|
||||
synchronized (this.connectedPeers) {
|
||||
return this.connectedPeers.stream()
|
||||
.filter(peer -> peer.getHandshakeStatus() == Handshake.COMPLETED)
|
||||
.collect(Collectors.toList());
|
||||
public List<Peer> getImmutableHandshakedPeers() {
|
||||
return this.immutableHandshakedPeers;
|
||||
}
|
||||
|
||||
public void addHandshakedPeer(Peer peer) {
|
||||
this.handshakedPeers.add(peer); // thread safe thanks to synchronized list
|
||||
this.immutableHandshakedPeers = List.copyOf(this.handshakedPeers); // also thread safe thanks to synchronized collection's toArray() being fed to List.of(array)
|
||||
|
||||
// Also add to outbound handshaked peers cache
|
||||
if (peer.isOutbound()) {
|
||||
this.addOutboundHandshakedPeer(peer);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeHandshakedPeer(Peer peer) {
|
||||
this.handshakedPeers.remove(peer); // thread safe thanks to synchronized list
|
||||
this.immutableHandshakedPeers = List.copyOf(this.handshakedPeers); // also thread safe thanks to synchronized collection's toArray() being fed to List.of(array)
|
||||
|
||||
// Also remove from outbound handshaked peers cache
|
||||
if (peer.isOutbound()) {
|
||||
this.removeOutboundHandshakedPeer(peer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of peers we connected to that have completed handshaking.
|
||||
*/
|
||||
public List<Peer> getOutboundHandshakedPeers() {
|
||||
synchronized (this.connectedPeers) {
|
||||
return this.connectedPeers.stream()
|
||||
.filter(peer -> peer.isOutbound() && peer.getHandshakeStatus() == Handshake.COMPLETED)
|
||||
.collect(Collectors.toList());
|
||||
public List<Peer> getImmutableOutboundHandshakedPeers() {
|
||||
return this.immutableOutboundHandshakedPeers;
|
||||
}
|
||||
|
||||
public void addOutboundHandshakedPeer(Peer peer) {
|
||||
if (!peer.isOutbound()) {
|
||||
return;
|
||||
}
|
||||
this.outboundHandshakedPeers.add(peer); // thread safe thanks to synchronized list
|
||||
this.immutableOutboundHandshakedPeers = List.copyOf(this.outboundHandshakedPeers); // also thread safe thanks to synchronized collection's toArray() being fed to List.of(array)
|
||||
}
|
||||
|
||||
public void removeOutboundHandshakedPeer(Peer peer) {
|
||||
if (!peer.isOutbound()) {
|
||||
return;
|
||||
}
|
||||
this.outboundHandshakedPeers.remove(peer); // thread safe thanks to synchronized list
|
||||
this.immutableOutboundHandshakedPeers = List.copyOf(this.outboundHandshakedPeers); // also thread safe thanks to synchronized collection's toArray() being fed to List.of(array)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first peer that has completed handshaking and has matching public key.
|
||||
*/
|
||||
public Peer getHandshakedPeerWithPublicKey(byte[] publicKey) {
|
||||
synchronized (this.connectedPeers) {
|
||||
return this.connectedPeers.stream()
|
||||
.filter(peer -> peer.getHandshakeStatus() == Handshake.COMPLETED
|
||||
&& Arrays.equals(peer.getPeersPublicKey(), publicKey))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
return this.getImmutableConnectedPeers().stream()
|
||||
.filter(peer -> peer.getHandshakeStatus() == Handshake.COMPLETED
|
||||
&& Arrays.equals(peer.getPeersPublicKey(), publicKey))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
// Peer list filters
|
||||
@@ -359,21 +436,15 @@ public class Network {
|
||||
return this.selfPeers.stream().anyMatch(selfPeer -> selfPeer.equals(peerAddress));
|
||||
};
|
||||
|
||||
/**
|
||||
* Must be inside <tt>synchronized (this.connectedPeers) {...}</tt>
|
||||
*/
|
||||
private final Predicate<PeerData> isConnectedPeer = peerData -> {
|
||||
PeerAddress peerAddress = peerData.getAddress();
|
||||
return this.connectedPeers.stream().anyMatch(peer -> peer.getPeerData().getAddress().equals(peerAddress));
|
||||
return this.getImmutableConnectedPeers().stream().anyMatch(peer -> peer.getPeerData().getAddress().equals(peerAddress));
|
||||
};
|
||||
|
||||
/**
|
||||
* Must be inside <tt>synchronized (this.connectedPeers) {...}</tt>
|
||||
*/
|
||||
private final Predicate<PeerData> isResolvedAsConnectedPeer = peerData -> {
|
||||
try {
|
||||
InetSocketAddress resolvedSocketAddress = peerData.getAddress().toSocketAddress();
|
||||
return this.connectedPeers.stream()
|
||||
return this.getImmutableConnectedPeers().stream()
|
||||
.anyMatch(peer -> peer.getResolvedAddress().equals(resolvedSocketAddress));
|
||||
} catch (UnknownHostException e) {
|
||||
// Can't resolve - no point even trying to connect
|
||||
@@ -398,6 +469,11 @@ public class Network {
|
||||
|
||||
class NetworkProcessor extends ExecuteProduceConsume {
|
||||
|
||||
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
|
||||
|
||||
private Iterator<SelectionKey> channelIterator = null;
|
||||
|
||||
NetworkProcessor(ExecutorService executor) {
|
||||
super(executor);
|
||||
}
|
||||
@@ -439,51 +515,31 @@ public class Network {
|
||||
}
|
||||
|
||||
private Task maybeProducePeerMessageTask() {
|
||||
for (Peer peer : getConnectedPeers()) {
|
||||
Task peerTask = peer.getMessageTask();
|
||||
if (peerTask != null) {
|
||||
return peerTask;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return getImmutableConnectedPeers().stream()
|
||||
.map(Peer::getMessageTask)
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Task maybeProducePeerPingTask(Long now) {
|
||||
// Ask connected peers whether they need a ping
|
||||
for (Peer peer : getHandshakedPeers()) {
|
||||
Task peerTask = peer.getPingTask(now);
|
||||
if (peerTask != null) {
|
||||
return peerTask;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class PeerConnectTask implements ExecuteProduceConsume.Task {
|
||||
private final Peer peer;
|
||||
|
||||
PeerConnectTask(Peer peer) {
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform() throws InterruptedException {
|
||||
connectPeer(peer);
|
||||
}
|
||||
return getImmutableHandshakedPeers().stream()
|
||||
.map(peer -> peer.getPingTask(now))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Task maybeProduceConnectPeerTask(Long now) throws InterruptedException {
|
||||
if (now == null || now < nextConnectTaskTimestamp) {
|
||||
if (now == null || now < nextConnectTaskTimestamp.get()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (getOutboundHandshakedPeers().size() >= minOutboundPeers) {
|
||||
if (getImmutableOutboundHandshakedPeers().size() >= minOutboundPeers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
nextConnectTaskTimestamp = now + 1000L;
|
||||
nextConnectTaskTimestamp.set(now + 1000L);
|
||||
|
||||
Peer targetPeer = getConnectablePeer(now);
|
||||
if (targetPeer == null) {
|
||||
@@ -495,66 +551,15 @@ public class Network {
|
||||
}
|
||||
|
||||
private Task maybeProduceBroadcastTask(Long now) {
|
||||
if (now == null || now < nextBroadcastTimestamp) {
|
||||
if (now == null || now < nextBroadcastTimestamp.get()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
nextBroadcastTimestamp = now + BROADCAST_INTERVAL;
|
||||
return () -> Controller.getInstance().doNetworkBroadcast();
|
||||
}
|
||||
|
||||
class ChannelTask implements ExecuteProduceConsume.Task {
|
||||
private final SelectionKey selectionKey;
|
||||
|
||||
ChannelTask(SelectionKey selectionKey) {
|
||||
this.selectionKey = selectionKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform() throws InterruptedException {
|
||||
try {
|
||||
LOGGER.trace("Thread {} has pending channel: {}, with ops {}",
|
||||
Thread.currentThread().getId(), selectionKey.channel(), selectionKey.readyOps());
|
||||
|
||||
// process pending channel task
|
||||
if (selectionKey.isReadable()) {
|
||||
connectionRead((SocketChannel) selectionKey.channel());
|
||||
} else if (selectionKey.isAcceptable()) {
|
||||
acceptConnection((ServerSocketChannel) selectionKey.channel());
|
||||
}
|
||||
|
||||
LOGGER.trace("Thread {} processed channel: {}",
|
||||
Thread.currentThread().getId(), selectionKey.channel());
|
||||
} catch (CancelledKeyException e) {
|
||||
LOGGER.trace("Thread {} encountered cancelled channel: {}",
|
||||
Thread.currentThread().getId(), selectionKey.channel());
|
||||
}
|
||||
}
|
||||
|
||||
private void connectionRead(SocketChannel socketChannel) {
|
||||
Peer peer = getPeerFromChannel(socketChannel);
|
||||
if (peer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
peer.readChannel();
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage() != null && e.getMessage().toLowerCase().contains("connection reset")) {
|
||||
peer.disconnect("Connection reset");
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.trace("[{}] Network thread {} encountered I/O error: {}", peer.getPeerConnectionId(),
|
||||
Thread.currentThread().getId(), e.getMessage(), e);
|
||||
peer.disconnect("I/O error");
|
||||
}
|
||||
}
|
||||
nextBroadcastTimestamp.set(now + BROADCAST_INTERVAL);
|
||||
return new BroadcastTask();
|
||||
}
|
||||
|
||||
private Task maybeProduceChannelTask(boolean canBlock) throws InterruptedException {
|
||||
final SelectionKey nextSelectionKey;
|
||||
|
||||
// Synchronization here to enforce thread-safety on channelIterator
|
||||
synchronized (channelSelector) {
|
||||
// anything to do?
|
||||
@@ -575,92 +580,73 @@ public class Network {
|
||||
}
|
||||
|
||||
channelIterator = channelSelector.selectedKeys().iterator();
|
||||
LOGGER.trace("Thread {}, after {} select, channelIterator now {}",
|
||||
Thread.currentThread().getId(),
|
||||
canBlock ? "blocking": "non-blocking",
|
||||
channelIterator);
|
||||
}
|
||||
|
||||
if (channelIterator.hasNext()) {
|
||||
nextSelectionKey = channelIterator.next();
|
||||
channelIterator.remove();
|
||||
} else {
|
||||
nextSelectionKey = null;
|
||||
if (!channelIterator.hasNext()) {
|
||||
channelIterator = null; // Nothing to do so reset iterator to cause new select
|
||||
|
||||
LOGGER.trace("Thread {}, channelIterator now null", Thread.currentThread().getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
LOGGER.trace("Thread {}, nextSelectionKey {}, channelIterator now {}",
|
||||
Thread.currentThread().getId(), nextSelectionKey, channelIterator);
|
||||
}
|
||||
final SelectionKey nextSelectionKey = channelIterator.next();
|
||||
channelIterator.remove();
|
||||
|
||||
if (nextSelectionKey == null) {
|
||||
return null;
|
||||
}
|
||||
// Just in case underlying socket channel already closed elsewhere, etc.
|
||||
if (!nextSelectionKey.isValid())
|
||||
return null;
|
||||
|
||||
return new ChannelTask(nextSelectionKey);
|
||||
}
|
||||
}
|
||||
LOGGER.trace("Thread {}, nextSelectionKey {}", Thread.currentThread().getId(), nextSelectionKey);
|
||||
|
||||
private void acceptConnection(ServerSocketChannel serverSocketChannel) throws InterruptedException {
|
||||
SocketChannel socketChannel;
|
||||
SelectableChannel socketChannel = nextSelectionKey.channel();
|
||||
|
||||
try {
|
||||
socketChannel = serverSocketChannel.accept();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No connection actually accepted?
|
||||
if (socketChannel == null) {
|
||||
return;
|
||||
}
|
||||
PeerAddress address = PeerAddress.fromSocket(socketChannel.socket());
|
||||
List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
|
||||
if (fixedNetwork != null && !fixedNetwork.isEmpty() && ipNotInFixedList(address, fixedNetwork)) {
|
||||
try {
|
||||
LOGGER.debug("Connection discarded from peer {} as not in the fixed network list", address);
|
||||
socketChannel.close();
|
||||
} catch (IOException e) {
|
||||
// IGNORE
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final Long now = NTP.getTime();
|
||||
Peer newPeer;
|
||||
|
||||
try {
|
||||
if (now == null) {
|
||||
LOGGER.debug("Connection discarded from peer {} due to lack of NTP sync", address);
|
||||
socketChannel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this.connectedPeers) {
|
||||
if (connectedPeers.size() >= maxPeers) {
|
||||
// We have enough peers
|
||||
LOGGER.debug("Connection discarded from peer {} because the server is full", address);
|
||||
socketChannel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Connection accepted from peer {}", address);
|
||||
|
||||
newPeer = new Peer(socketChannel, channelSelector);
|
||||
this.connectedPeers.add(newPeer);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (socketChannel.isOpen()) {
|
||||
try {
|
||||
LOGGER.debug("Connection failed from peer {} while connecting/closing", address);
|
||||
socketChannel.close();
|
||||
} catch (IOException ce) {
|
||||
// Couldn't close?
|
||||
if (nextSelectionKey.isReadable()) {
|
||||
clearInterestOps(nextSelectionKey, SelectionKey.OP_READ);
|
||||
Peer peer = getPeerFromChannel((SocketChannel) socketChannel);
|
||||
if (peer == null)
|
||||
return null;
|
||||
|
||||
return new ChannelReadTask((SocketChannel) socketChannel, peer);
|
||||
}
|
||||
|
||||
if (nextSelectionKey.isWritable()) {
|
||||
clearInterestOps(nextSelectionKey, SelectionKey.OP_WRITE);
|
||||
Peer peer = getPeerFromChannel((SocketChannel) socketChannel);
|
||||
if (peer == null)
|
||||
return null;
|
||||
|
||||
// Any thread that queues a message to send can set OP_WRITE,
|
||||
// but we only allow one pending/active ChannelWriteTask per Peer
|
||||
if (!channelsPendingWrite.add(socketChannel))
|
||||
return null;
|
||||
|
||||
return new ChannelWriteTask((SocketChannel) socketChannel, peer);
|
||||
}
|
||||
|
||||
if (nextSelectionKey.isAcceptable()) {
|
||||
clearInterestOps(nextSelectionKey, SelectionKey.OP_ACCEPT);
|
||||
return new ChannelAcceptTask((ServerSocketChannel) socketChannel);
|
||||
}
|
||||
} catch (CancelledKeyException e) {
|
||||
/*
|
||||
* Sometimes nextSelectionKey is cancelled / becomes invalid between the isValid() test at line 586
|
||||
* and later calls to isReadable() / isWritable() / isAcceptable() which themselves call isValid()!
|
||||
* Those isXXXable() calls could throw CancelledKeyException, so we catch it here and return null.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.onPeerReady(newPeer);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ipNotInFixedList(PeerAddress address, List<String> fixedNetwork) {
|
||||
public boolean ipNotInFixedList(PeerAddress address, List<String> fixedNetwork) {
|
||||
for (String ipAddress : fixedNetwork) {
|
||||
String[] bits = ipAddress.split(":");
|
||||
if (bits.length >= 1 && bits.length <= 2 && address.getHost().equals(bits[0])) {
|
||||
@@ -692,16 +678,15 @@ public class Network {
|
||||
peers.removeIf(isSelfPeer);
|
||||
}
|
||||
|
||||
synchronized (this.connectedPeers) {
|
||||
// Don't consider already connected peers (simple address match)
|
||||
peers.removeIf(isConnectedPeer);
|
||||
// Don't consider already connected peers (simple address match)
|
||||
peers.removeIf(isConnectedPeer);
|
||||
|
||||
// Don't consider already connected peers (resolved address match)
|
||||
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
|
||||
peers.removeIf(isResolvedAsConnectedPeer);
|
||||
// Don't consider already connected peers (resolved address match)
|
||||
// Disabled because this might be too slow if we end up waiting a long time for hostnames to resolve via DNS
|
||||
// Which is ok because duplicate connections to the same peer are handled during handshaking
|
||||
// peers.removeIf(isResolvedAsConnectedPeer);
|
||||
|
||||
this.checkLongestConnection(now);
|
||||
}
|
||||
this.checkLongestConnection(now);
|
||||
|
||||
// Any left?
|
||||
if (peers.isEmpty()) {
|
||||
@@ -714,6 +699,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);
|
||||
@@ -729,8 +715,12 @@ public class Network {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean connectPeer(Peer newPeer) throws InterruptedException {
|
||||
SocketChannel socketChannel = newPeer.connect(this.channelSelector);
|
||||
public boolean connectPeer(Peer newPeer) throws InterruptedException {
|
||||
// Also checked before creating PeerConnectTask
|
||||
if (getImmutableOutboundHandshakedPeers().size() >= minOutboundPeers)
|
||||
return false;
|
||||
|
||||
SocketChannel socketChannel = newPeer.connect();
|
||||
if (socketChannel == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -739,21 +729,16 @@ public class Network {
|
||||
return false;
|
||||
}
|
||||
|
||||
synchronized (this.connectedPeers) {
|
||||
this.connectedPeers.add(newPeer);
|
||||
}
|
||||
|
||||
this.addConnectedPeer(newPeer);
|
||||
this.onPeerReady(newPeer);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Peer getPeerFromChannel(SocketChannel socketChannel) {
|
||||
synchronized (this.connectedPeers) {
|
||||
for (Peer peer : this.connectedPeers) {
|
||||
if (peer.getSocketChannel() == socketChannel) {
|
||||
return peer;
|
||||
}
|
||||
public Peer getPeerFromChannel(SocketChannel socketChannel) {
|
||||
for (Peer peer : this.getImmutableConnectedPeers()) {
|
||||
if (peer.getSocketChannel() == socketChannel) {
|
||||
return peer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,7 +751,7 @@ public class Network {
|
||||
}
|
||||
|
||||
// Find peers that have reached their maximum connection age, and disconnect them
|
||||
List<Peer> peersToDisconnect = this.connectedPeers.stream()
|
||||
List<Peer> peersToDisconnect = this.getImmutableConnectedPeers().stream()
|
||||
.filter(peer -> !peer.isSyncInProgress())
|
||||
.filter(peer -> peer.hasReachedMaxConnectionAge())
|
||||
.collect(Collectors.toList());
|
||||
@@ -783,7 +768,74 @@ public class Network {
|
||||
nextDisconnectionCheck = now + DISCONNECTION_CHECK_INTERVAL;
|
||||
}
|
||||
|
||||
// Peer callbacks
|
||||
// SocketChannel interest-ops manipulations
|
||||
|
||||
private static final String[] OP_NAMES = new String[SelectionKey.OP_ACCEPT * 2];
|
||||
static {
|
||||
for (int i = 0; i < OP_NAMES.length; i++) {
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
|
||||
if ((i & SelectionKey.OP_READ) != 0) joiner.add("OP_READ");
|
||||
if ((i & SelectionKey.OP_WRITE) != 0) joiner.add("OP_WRITE");
|
||||
if ((i & SelectionKey.OP_CONNECT) != 0) joiner.add("OP_CONNECT");
|
||||
if ((i & SelectionKey.OP_ACCEPT) != 0) joiner.add("OP_ACCEPT");
|
||||
|
||||
OP_NAMES[i] = joiner.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearInterestOps(SelectableChannel socketChannel, int interestOps) {
|
||||
SelectionKey selectionKey = socketChannel.keyFor(channelSelector);
|
||||
if (selectionKey == null)
|
||||
return;
|
||||
|
||||
clearInterestOps(selectionKey, interestOps);
|
||||
}
|
||||
|
||||
private void clearInterestOps(SelectionKey selectionKey, int interestOps) {
|
||||
if (!selectionKey.channel().isOpen())
|
||||
return;
|
||||
|
||||
LOGGER.trace("Thread {} clearing {} interest-ops on channel: {}",
|
||||
Thread.currentThread().getId(),
|
||||
OP_NAMES[interestOps],
|
||||
selectionKey.channel());
|
||||
|
||||
selectionKey.interestOpsAnd(~interestOps);
|
||||
}
|
||||
|
||||
public void setInterestOps(SelectableChannel socketChannel, int interestOps) {
|
||||
SelectionKey selectionKey = socketChannel.keyFor(channelSelector);
|
||||
if (selectionKey == null) {
|
||||
try {
|
||||
selectionKey = socketChannel.register(this.channelSelector, interestOps);
|
||||
} catch (ClosedChannelException e) {
|
||||
// Channel already closed so ignore
|
||||
return;
|
||||
}
|
||||
// Fall-through to allow logging
|
||||
}
|
||||
|
||||
setInterestOps(selectionKey, interestOps);
|
||||
}
|
||||
|
||||
private void setInterestOps(SelectionKey selectionKey, int interestOps) {
|
||||
if (!selectionKey.channel().isOpen())
|
||||
return;
|
||||
|
||||
LOGGER.trace("Thread {} setting {} interest-ops on channel: {}",
|
||||
Thread.currentThread().getId(),
|
||||
OP_NAMES[interestOps],
|
||||
selectionKey.channel());
|
||||
|
||||
selectionKey.interestOpsOr(interestOps);
|
||||
}
|
||||
|
||||
// Peer / Task callbacks
|
||||
|
||||
public void notifyChannelNotWriting(SelectableChannel socketChannel) {
|
||||
this.channelsPendingWrite.remove(socketChannel);
|
||||
}
|
||||
|
||||
protected void wakeupChannelSelector() {
|
||||
this.channelSelector.wakeup();
|
||||
@@ -809,17 +861,32 @@ public class Network {
|
||||
}
|
||||
|
||||
public void onDisconnect(Peer peer) {
|
||||
// Notify Controller
|
||||
Controller.getInstance().onPeerDisconnect(peer);
|
||||
if (peer.getConnectionEstablishedTime() > 0L) {
|
||||
LOGGER.debug("[{}] Disconnected from peer {}", peer.getPeerConnectionId(), peer);
|
||||
} else {
|
||||
LOGGER.debug("[{}] Failed to connect to peer {}", peer.getPeerConnectionId(), peer);
|
||||
}
|
||||
|
||||
synchronized (this.connectedPeers) {
|
||||
this.connectedPeers.remove(peer);
|
||||
this.removeConnectedPeer(peer);
|
||||
this.channelsPendingWrite.remove(peer.getSocketChannel());
|
||||
|
||||
if (this.isShuttingDown)
|
||||
// No need to do any further processing, like re-enabling listen socket or notifying Controller
|
||||
return;
|
||||
|
||||
if (getImmutableConnectedPeers().size() < maxPeers - 1
|
||||
&& serverSelectionKey.isValid()
|
||||
&& (serverSelectionKey.interestOps() & SelectionKey.OP_ACCEPT) == 0) {
|
||||
try {
|
||||
LOGGER.debug("Re-enabling accepting incoming connections because the server is not longer full");
|
||||
setInterestOps(serverSelectionKey, SelectionKey.OP_ACCEPT);
|
||||
} catch (CancelledKeyException e) {
|
||||
LOGGER.error("Failed to re-enable accepting of incoming connections: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Notify Controller
|
||||
Controller.getInstance().onPeerDisconnect(peer);
|
||||
}
|
||||
|
||||
public void peerMisbehaved(Peer peer) {
|
||||
@@ -980,6 +1047,9 @@ public class Network {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to handshaked peers cache
|
||||
this.addHandshakedPeer(peer);
|
||||
|
||||
// Make a note that we've successfully completed handshake (and when)
|
||||
peer.getPeerData().setLastConnected(NTP.getTime());
|
||||
|
||||
@@ -1119,6 +1189,7 @@ public class Network {
|
||||
return;
|
||||
}
|
||||
String host = parts[0];
|
||||
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(host);
|
||||
if (addr.isAnyLocalAddress() || addr.isSiteLocalAddress()) {
|
||||
@@ -1129,6 +1200,9 @@ public class Network {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep track of the port
|
||||
this.ourExternalPort = Integer.parseInt(parts[1]);
|
||||
|
||||
// Add to the list
|
||||
this.ourExternalIpAddressHistory.add(host);
|
||||
|
||||
@@ -1169,19 +1243,32 @@ public class Network {
|
||||
if (consecutiveReadings >= consecutiveReadingsRequired) {
|
||||
// Last 10 readings were the same - i.e. more than one peer agreed on the new IP address...
|
||||
String ip = ipAddressHistory.get(size - 1);
|
||||
if (!Objects.equals(ip, this.ourExternalIpAddress)) {
|
||||
// ... and the readings were different to our current recorded value, so
|
||||
// update our external IP address value
|
||||
this.ourExternalIpAddress = ip;
|
||||
this.onExternalIpUpdate(ip);
|
||||
if (ip != null && !Objects.equals(ip, "null")) {
|
||||
if (!Objects.equals(ip, this.ourExternalIpAddress)) {
|
||||
// ... and the readings were different to our current recorded value, so
|
||||
// update our external IP address value
|
||||
this.ourExternalIpAddress = ip;
|
||||
this.onExternalIpUpdate(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onExternalIpUpdate(String ipAddress) {
|
||||
LOGGER.info("External IP address updated to {}", ipAddress);
|
||||
}
|
||||
|
||||
ArbitraryDataManager.getInstance().broadcastHostedSignatureList();
|
||||
public String getOurExternalIpAddress() {
|
||||
// FUTURE: replace port if UPnP is active, as it will be more accurate
|
||||
return this.ourExternalIpAddress;
|
||||
}
|
||||
|
||||
public String getOurExternalIpAddressAndPort() {
|
||||
String ipAddress = this.getOurExternalIpAddress();
|
||||
if (ipAddress == null) {
|
||||
return null;
|
||||
}
|
||||
return String.format("%s:%d", ipAddress, this.ourExternalPort);
|
||||
}
|
||||
|
||||
|
||||
@@ -1225,7 +1312,7 @@ public class Network {
|
||||
}
|
||||
}
|
||||
|
||||
for (Peer peer : this.getConnectedPeers()) {
|
||||
for (Peer peer : this.getImmutableConnectedPeers()) {
|
||||
peer.disconnect("to be forgotten");
|
||||
}
|
||||
|
||||
@@ -1237,8 +1324,9 @@ public class Network {
|
||||
try {
|
||||
InetSocketAddress knownAddress = peerAddress.toSocketAddress();
|
||||
|
||||
List<Peer> peers = this.getConnectedPeers();
|
||||
peers.removeIf(peer -> !Peer.addressEquals(knownAddress, peer.getResolvedAddress()));
|
||||
List<Peer> peers = this.getImmutableConnectedPeers().stream()
|
||||
.filter(peer -> Peer.addressEquals(knownAddress, peer.getResolvedAddress()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Peer peer : peers) {
|
||||
peer.disconnect("to be forgotten");
|
||||
@@ -1257,7 +1345,8 @@ public class Network {
|
||||
}
|
||||
|
||||
// Disconnect peers that are stuck during handshake
|
||||
List<Peer> handshakePeers = this.getConnectedPeers();
|
||||
// Needs a mutable copy of the unmodifiableList
|
||||
List<Peer> handshakePeers = new ArrayList<>(this.getImmutableConnectedPeers());
|
||||
|
||||
// Disregard peers that have completed handshake or only connected recently
|
||||
handshakePeers.removeIf(peer -> peer.getHandshakeStatus() == Handshake.COMPLETED
|
||||
@@ -1299,9 +1388,7 @@ public class Network {
|
||||
peers.removeIf(isNotOldPeer);
|
||||
|
||||
// Don't consider already connected peers (simple address match)
|
||||
synchronized (this.connectedPeers) {
|
||||
peers.removeIf(isConnectedPeer);
|
||||
}
|
||||
peers.removeIf(isConnectedPeer);
|
||||
|
||||
for (PeerData peerData : peers) {
|
||||
LOGGER.debug("Deleting old peer {} from repository", peerData.getAddress().toString());
|
||||
@@ -1397,54 +1484,27 @@ public class Network {
|
||||
}
|
||||
|
||||
public void broadcast(Function<Peer, Message> peerMessageBuilder) {
|
||||
class Broadcaster implements Runnable {
|
||||
private final Random random = new Random();
|
||||
for (Peer peer : getImmutableHandshakedPeers()) {
|
||||
if (this.isShuttingDown)
|
||||
return;
|
||||
|
||||
private List<Peer> targetPeers;
|
||||
private Function<Peer, Message> peerMessageBuilder;
|
||||
Message message = peerMessageBuilder.apply(peer);
|
||||
|
||||
Broadcaster(List<Peer> targetPeers, Function<Peer, Message> peerMessageBuilder) {
|
||||
this.targetPeers = targetPeers;
|
||||
this.peerMessageBuilder = peerMessageBuilder;
|
||||
if (message == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Network Broadcast");
|
||||
|
||||
for (Peer peer : targetPeers) {
|
||||
// Very short sleep to reduce strain, improve multi-threading and catch interrupts
|
||||
try {
|
||||
Thread.sleep(random.nextInt(20) + 20L);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
Message message = peerMessageBuilder.apply(peer);
|
||||
|
||||
if (message == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!peer.sendMessage(message)) {
|
||||
peer.disconnect("failed to broadcast message");
|
||||
}
|
||||
}
|
||||
|
||||
Thread.currentThread().setName("Network Broadcast (dormant)");
|
||||
if (!peer.sendMessage(message)) {
|
||||
peer.disconnect("failed to broadcast message");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
broadcastExecutor.execute(new Broadcaster(this.getHandshakedPeers(), peerMessageBuilder));
|
||||
} catch (RejectedExecutionException e) {
|
||||
// Can't execute - probably because we're shutting down, so ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown
|
||||
|
||||
public void shutdown() {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
// Close listen socket to prevent more incoming connections
|
||||
if (this.serverChannel.isOpen()) {
|
||||
try {
|
||||
@@ -1463,18 +1523,8 @@ public class Network {
|
||||
LOGGER.warn("Interrupted while waiting for networking threads to terminate");
|
||||
}
|
||||
|
||||
// Stop broadcasts
|
||||
this.broadcastExecutor.shutdownNow();
|
||||
try {
|
||||
if (!this.broadcastExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS)) {
|
||||
LOGGER.warn("Broadcast threads failed to terminate");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.warn("Interrupted while waiting for broadcast threads failed to terminate");
|
||||
}
|
||||
|
||||
// Close all peer connections
|
||||
for (Peer peer : this.getConnectedPeers()) {
|
||||
for (Peer peer : this.getImmutableConnectedPeers()) {
|
||||
peer.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,25 +11,21 @@ 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.Message.MessageException;
|
||||
import org.qortal.network.message.Message.MessageType;
|
||||
import org.qortal.network.message.PingMessage;
|
||||
import org.qortal.network.message.MessageException;
|
||||
import org.qortal.network.task.MessageTask;
|
||||
import org.qortal.network.task.PingTask;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ExecuteProduceConsume;
|
||||
import org.qortal.utils.ExecuteProduceConsume.Task;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -48,9 +44,9 @@ public class Peer {
|
||||
private static final int RESPONSE_TIMEOUT = 3000; // ms
|
||||
|
||||
/**
|
||||
* Maximum time to wait for a peer to respond with blocks (ms)
|
||||
* Maximum time to wait for a message to be added to sendQueue (ms)
|
||||
*/
|
||||
public static final int FETCH_BLOCKS_TIMEOUT = 10000;
|
||||
private static final int QUEUE_TIMEOUT = 1000; // ms
|
||||
|
||||
/**
|
||||
* Interval between PING messages to a peer. (ms)
|
||||
@@ -68,13 +64,22 @@ 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;
|
||||
|
||||
private Map<Integer, BlockingQueue<Message>> replyQueues;
|
||||
private LinkedBlockingQueue<Message> pendingMessages;
|
||||
|
||||
private TransferQueue<Message> sendQueue;
|
||||
private ByteBuffer outputBuffer;
|
||||
private String outputMessageType;
|
||||
private int outputMessageId;
|
||||
|
||||
/**
|
||||
* True if we created connection to peer, false if we accepted incoming connection from peer.
|
||||
*/
|
||||
@@ -98,7 +103,7 @@ public class Peer {
|
||||
/**
|
||||
* When last PING message was sent, or null if pings not started yet.
|
||||
*/
|
||||
private Long lastPingSent;
|
||||
private Long lastPingSent = null;
|
||||
|
||||
byte[] ourChallenge;
|
||||
|
||||
@@ -160,10 +165,10 @@ public class Peer {
|
||||
/**
|
||||
* Construct Peer using existing, connected socket
|
||||
*/
|
||||
public Peer(SocketChannel socketChannel, Selector channelSelector) throws IOException {
|
||||
public Peer(SocketChannel socketChannel) throws IOException {
|
||||
this.isOutbound = false;
|
||||
this.socketChannel = socketChannel;
|
||||
sharedSetup(channelSelector);
|
||||
sharedSetup();
|
||||
|
||||
this.resolvedAddress = ((InetSocketAddress) socketChannel.socket().getRemoteSocketAddress());
|
||||
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
|
||||
@@ -194,6 +199,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 +224,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();
|
||||
@@ -276,7 +294,7 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
protected void setLastPing(long lastPing) {
|
||||
public void setLastPing(long lastPing) {
|
||||
synchronized (this.peerInfoLock) {
|
||||
this.lastPing = lastPing;
|
||||
}
|
||||
@@ -346,12 +364,6 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
protected void queueMessage(Message message) {
|
||||
if (!this.pendingMessages.offer(message)) {
|
||||
LOGGER.info("[{}] No room to queue message from peer {} - discarding", this.peerConnectionId, this);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSyncInProgress() {
|
||||
return this.syncInProgress;
|
||||
}
|
||||
@@ -396,13 +408,14 @@ public class Peer {
|
||||
|
||||
// Processing
|
||||
|
||||
private void sharedSetup(Selector channelSelector) throws IOException {
|
||||
private void sharedSetup() throws IOException {
|
||||
this.connectionTimestamp = NTP.getTime();
|
||||
this.socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
|
||||
this.socketChannel.configureBlocking(false);
|
||||
this.socketChannel.register(channelSelector, SelectionKey.OP_READ);
|
||||
Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_READ);
|
||||
this.byteBuffer = null; // Defer allocation to when we need it, to save memory. Sorry GC!
|
||||
this.replyQueues = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
|
||||
this.sendQueue = new LinkedTransferQueue<>();
|
||||
this.replyQueues = new ConcurrentHashMap<>();
|
||||
this.pendingMessages = new LinkedBlockingQueue<>();
|
||||
|
||||
Random random = new SecureRandom();
|
||||
@@ -410,7 +423,7 @@ public class Peer {
|
||||
random.nextBytes(this.ourChallenge);
|
||||
}
|
||||
|
||||
public SocketChannel connect(Selector channelSelector) {
|
||||
public SocketChannel connect() {
|
||||
LOGGER.trace("[{}] Connecting to peer {}", this.peerConnectionId, this);
|
||||
|
||||
try {
|
||||
@@ -418,6 +431,8 @@ public class Peer {
|
||||
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
|
||||
|
||||
this.socketChannel = SocketChannel.open();
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
this.socketChannel.socket().bind(new InetSocketAddress(bindAddr, 0));
|
||||
this.socketChannel.socket().connect(resolvedAddress, CONNECT_TIMEOUT);
|
||||
} catch (SocketTimeoutException e) {
|
||||
LOGGER.trace("[{}] Connection timed out to peer {}", this.peerConnectionId, this);
|
||||
@@ -432,7 +447,7 @@ public class Peer {
|
||||
|
||||
try {
|
||||
LOGGER.debug("[{}] Connected to peer {}", this.peerConnectionId, this);
|
||||
sharedSetup(channelSelector);
|
||||
sharedSetup();
|
||||
return socketChannel;
|
||||
} catch (IOException e) {
|
||||
LOGGER.trace("[{}] Post-connection setup failed, peer {}", this.peerConnectionId, this);
|
||||
@@ -450,7 +465,7 @@ public class Peer {
|
||||
*
|
||||
* @throws IOException If this channel is not yet connected
|
||||
*/
|
||||
protected void readChannel() throws IOException {
|
||||
public void readChannel() throws IOException {
|
||||
synchronized (this.byteBufferLock) {
|
||||
while (true) {
|
||||
if (!this.socketChannel.isOpen() || this.socketChannel.socket().isClosed()) {
|
||||
@@ -556,7 +571,67 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
protected ExecuteProduceConsume.Task getMessageTask() {
|
||||
/** Maybe send some pending outgoing messages.
|
||||
*
|
||||
* @return true if more data is pending to be sent
|
||||
*/
|
||||
public boolean writeChannel() throws IOException {
|
||||
// It is the responsibility of ChannelWriteTask's producer to produce only one call to writeChannel() at a time
|
||||
|
||||
while (true) {
|
||||
// If output byte buffer is null, fetch next message from queue (if any)
|
||||
while (this.outputBuffer == null) {
|
||||
Message message;
|
||||
|
||||
try {
|
||||
// Allow other thread time to add message to queue having raised OP_WRITE.
|
||||
// Timeout is overkill but not excessive enough to clog up networking / EPC.
|
||||
// This is to avoid race condition in sendMessageWithTimeout() below.
|
||||
message = this.sendQueue.poll(QUEUE_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// Shutdown situation
|
||||
return false;
|
||||
}
|
||||
|
||||
// No message? No further work to be done
|
||||
if (message == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
this.outputBuffer = ByteBuffer.wrap(message.toBytes());
|
||||
this.outputMessageType = message.getType().name();
|
||||
this.outputMessageId = message.getId();
|
||||
|
||||
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}",
|
||||
this.peerConnectionId, this.outputMessageType, this.outputMessageId, this);
|
||||
} 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,
|
||||
message.getType().name(), message.getId(), this, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// If output byte buffer is not null, send from that
|
||||
int bytesWritten = this.socketChannel.write(outputBuffer);
|
||||
|
||||
LOGGER.trace("[{}] Sent {} bytes of {} message with ID {} to peer {} ({} total)", this.peerConnectionId,
|
||||
bytesWritten, this.outputMessageType, this.outputMessageId, this, outputBuffer.limit());
|
||||
|
||||
// If we've sent 0 bytes then socket buffer is full so we need to wait until it's empty again
|
||||
if (bytesWritten == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we then exhaust the byte buffer, set it to null (otherwise loop and try to send more)
|
||||
if (!this.outputBuffer.hasRemaining()) {
|
||||
this.outputMessageType = null;
|
||||
this.outputMessageId = 0;
|
||||
this.outputBuffer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Task getMessageTask() {
|
||||
/*
|
||||
* If we are still handshaking and there is a message yet to be processed then
|
||||
* don't produce another message task. This allows us to process handshake
|
||||
@@ -580,7 +655,7 @@ public class Peer {
|
||||
}
|
||||
|
||||
// Return a task to process message in queue
|
||||
return () -> Network.getInstance().onMessage(this, nextMessage);
|
||||
return new MessageTask(this, nextMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -605,54 +680,25 @@ public class Peer {
|
||||
}
|
||||
|
||||
try {
|
||||
// Send message
|
||||
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}", this.peerConnectionId,
|
||||
// Queue message, to be picked up by ChannelWriteTask and then peer.writeChannel()
|
||||
LOGGER.trace("[{}] Queuing {} message with ID {} to peer {}", this.peerConnectionId,
|
||||
message.getType().name(), message.getId(), this);
|
||||
|
||||
ByteBuffer outputBuffer = ByteBuffer.wrap(message.toBytes());
|
||||
// Check message properly constructed
|
||||
message.checkValidOutgoing();
|
||||
|
||||
synchronized (this.socketChannel) {
|
||||
final long sendStart = System.currentTimeMillis();
|
||||
long totalBytes = 0;
|
||||
|
||||
while (outputBuffer.hasRemaining()) {
|
||||
int bytesWritten = this.socketChannel.write(outputBuffer);
|
||||
totalBytes += bytesWritten;
|
||||
|
||||
LOGGER.trace("[{}] Sent {} bytes of {} message with ID {} to peer {} ({} total)", this.peerConnectionId,
|
||||
bytesWritten, message.getType().name(), message.getId(), this, totalBytes);
|
||||
|
||||
if (bytesWritten == 0) {
|
||||
// Underlying socket's internal buffer probably full,
|
||||
// so wait a short while for bytes to actually be transmitted over the wire
|
||||
|
||||
/*
|
||||
* NOSONAR squid:S2276 - we don't want to use this.socketChannel.wait()
|
||||
* as this releases the lock held by synchronized() above
|
||||
* and would allow another thread to send another message,
|
||||
* potentially interleaving them on-the-wire, causing checksum failures
|
||||
* and connection loss.
|
||||
*/
|
||||
Thread.sleep(1L); //NOSONAR squid:S2276
|
||||
|
||||
if (System.currentTimeMillis() - sendStart > timeout) {
|
||||
// We've taken too long to send this message
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (MessageException e) {
|
||||
LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
|
||||
message.getType().name(), message.getId(), this, e.getMessage());
|
||||
return false;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// Possible race condition:
|
||||
// We set OP_WRITE, EPC creates ChannelWriteTask which calls Peer.writeChannel, writeChannel's poll() finds no message to send
|
||||
// Avoided by poll-with-timeout in writeChannel() above.
|
||||
Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_WRITE);
|
||||
return this.sendQueue.tryTransfer(message, timeout, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// Send failure
|
||||
return false;
|
||||
} catch (MessageException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sent OK
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -720,7 +766,7 @@ public class Peer {
|
||||
this.lastPingSent = NTP.getTime();
|
||||
}
|
||||
|
||||
protected ExecuteProduceConsume.Task getPingTask(Long now) {
|
||||
protected Task getPingTask(Long now) {
|
||||
// Pings not enabled yet?
|
||||
if (now == null || this.lastPingSent == null) {
|
||||
return null;
|
||||
@@ -734,19 +780,7 @@ public class Peer {
|
||||
// Not strictly true, but prevents this peer from being immediately chosen again
|
||||
this.lastPingSent = now;
|
||||
|
||||
return () -> {
|
||||
PingMessage pingMessage = new PingMessage();
|
||||
Message message = this.getResponse(pingMessage);
|
||||
|
||||
if (message == null || message.getType() != MessageType.PING) {
|
||||
LOGGER.debug("[{}] Didn't receive reply from {} for PING ID {}", this.peerConnectionId, this,
|
||||
pingMessage.getId());
|
||||
this.disconnect("no ping received");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLastPing(NTP.getTime() - now);
|
||||
};
|
||||
return new PingTask(this, now);
|
||||
}
|
||||
|
||||
public void disconnect(String reason) {
|
||||
@@ -877,6 +911,10 @@ public class Peer {
|
||||
return maxConnectionAge;
|
||||
}
|
||||
|
||||
public void setMaxConnectionAge(long maxConnectionAge) {
|
||||
this.maxConnectionAge = maxConnectionAge;
|
||||
}
|
||||
|
||||
public boolean hasReachedMaxConnectionAge() {
|
||||
return this.getConnectionAge() > this.getMaxConnectionAge();
|
||||
}
|
||||
|
||||
@@ -1,90 +1,136 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Serialization;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ArbitraryDataFileListMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
private static final int HASH_LENGTH = Transformer.SHA256_LENGTH;
|
||||
private byte[] signature;
|
||||
private List<byte[]> hashes;
|
||||
private Long requestTime;
|
||||
private Integer requestHops;
|
||||
private String peerAddress;
|
||||
private Boolean isRelayPossible;
|
||||
|
||||
private final byte[] signature;
|
||||
private final List<byte[]> hashes;
|
||||
|
||||
public ArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes) {
|
||||
public ArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes, Long requestTime,
|
||||
Integer requestHops, String peerAddress, Boolean isRelayPossible) {
|
||||
super(MessageType.ARBITRARY_DATA_FILE_LIST);
|
||||
|
||||
this.signature = signature;
|
||||
this.hashes = hashes;
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(hashes.size()));
|
||||
|
||||
for (byte[] hash : hashes) {
|
||||
bytes.write(hash);
|
||||
}
|
||||
|
||||
if (requestTime != null) {
|
||||
// The remaining fields are optional
|
||||
|
||||
bytes.write(Longs.toByteArray(requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(requestHops));
|
||||
|
||||
Serialization.serializeSizedStringV2(bytes, peerAddress);
|
||||
|
||||
bytes.write(Ints.toByteArray(Boolean.TRUE.equals(isRelayPossible) ? 1 : 0));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public ArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes) {
|
||||
/** Legacy version */
|
||||
public ArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes) {
|
||||
this(signature, hashes, null, null, null, null);
|
||||
}
|
||||
|
||||
private ArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, Long requestTime,
|
||||
Integer requestHops, String peerAddress, boolean isRelayPossible) {
|
||||
super(id, MessageType.ARBITRARY_DATA_FILE_LIST);
|
||||
|
||||
this.signature = signature;
|
||||
this.hashes = hashes;
|
||||
}
|
||||
|
||||
public List<byte[]> getHashes() {
|
||||
return this.hashes;
|
||||
this.requestTime = requestTime;
|
||||
this.requestHops = requestHops;
|
||||
this.peerAddress = peerAddress;
|
||||
this.isRelayPossible = isRelayPossible;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public List<byte[]> getHashes() {
|
||||
return this.hashes;
|
||||
}
|
||||
|
||||
public Long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
public Integer getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public String getPeerAddress() {
|
||||
return this.peerAddress;
|
||||
}
|
||||
|
||||
public Boolean isRelayPossible() {
|
||||
return this.isRelayPossible;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
int count = bytes.getInt();
|
||||
|
||||
if (bytes.remaining() != count * HASH_LENGTH)
|
||||
return null;
|
||||
|
||||
List<byte[]> hashes = new ArrayList<>();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
|
||||
byte[] hash = new byte[HASH_LENGTH];
|
||||
byte[] hash = new byte[Transformer.SHA256_LENGTH];
|
||||
bytes.get(hash);
|
||||
hashes.add(hash);
|
||||
}
|
||||
|
||||
return new ArbitraryDataFileListMessage(id, signature, hashes);
|
||||
}
|
||||
Long requestTime = null;
|
||||
Integer requestHops = null;
|
||||
String peerAddress = null;
|
||||
boolean isRelayPossible = true; // Legacy versions only send this message when relaying is possible
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
// The remaining fields are optional
|
||||
if (bytes.hasRemaining()) {
|
||||
try {
|
||||
requestTime = bytes.getLong();
|
||||
|
||||
bytes.write(this.signature);
|
||||
requestHops = bytes.getInt();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.hashes.size()));
|
||||
peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
|
||||
|
||||
for (byte[] hash : this.hashes) {
|
||||
bytes.write(hash);
|
||||
isRelayPossible = bytes.getInt() > 0;
|
||||
} catch (TransformationException e) {
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ArbitraryDataFileListMessage cloneWithNewId(int newId) {
|
||||
ArbitraryDataFileListMessage clone = new ArbitraryDataFileListMessage(this.signature, this.hashes);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
return new ArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops, peerAddress, isRelayPossible);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,48 +1,68 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ArbitraryDataFileMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileMessage.class);
|
||||
|
||||
private final byte[] signature;
|
||||
private final ArbitraryDataFile arbitraryDataFile;
|
||||
private byte[] signature;
|
||||
private ArbitraryDataFile arbitraryDataFile;
|
||||
|
||||
public ArbitraryDataFileMessage(byte[] signature, ArbitraryDataFile arbitraryDataFile) {
|
||||
super(MessageType.ARBITRARY_DATA_FILE);
|
||||
|
||||
this.signature = signature;
|
||||
this.arbitraryDataFile = arbitraryDataFile;
|
||||
byte[] data = arbitraryDataFile.getBytes();
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public ArbitraryDataFileMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
|
||||
private ArbitraryDataFileMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
|
||||
super(id, MessageType.ARBITRARY_DATA_FILE);
|
||||
|
||||
this.signature = signature;
|
||||
this.arbitraryDataFile = arbitraryDataFile;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public ArbitraryDataFile getArbitraryDataFile() {
|
||||
return this.arbitraryDataFile;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
int dataLength = byteBuffer.getInt();
|
||||
|
||||
if (byteBuffer.remaining() != dataLength)
|
||||
return null;
|
||||
if (byteBuffer.remaining() < dataLength)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
byte[] data = new byte[dataLength];
|
||||
byteBuffer.get(data);
|
||||
@@ -50,42 +70,10 @@ public class ArbitraryDataFileMessage extends Message {
|
||||
try {
|
||||
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(data, signature);
|
||||
return new ArbitraryDataFileMessage(id, signature, arbitraryDataFile);
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to process received file: {}", e.getMessage());
|
||||
throw new MessageException("Unable to process received file: " + e.getMessage(), e);
|
||||
}
|
||||
catch (DataException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
if (this.arbitraryDataFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] data = this.arbitraryDataFile.getBytes();
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ArbitraryDataFileMessage cloneWithNewId(int newId) {
|
||||
ArbitraryDataFileMessage clone = new ArbitraryDataFileMessage(this.signature, this.arbitraryDataFile);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
@@ -11,13 +11,26 @@ import com.google.common.primitives.Ints;
|
||||
|
||||
public class ArbitraryDataMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private byte[] signature;
|
||||
private byte[] data;
|
||||
|
||||
public ArbitraryDataMessage(byte[] signature, byte[] data) {
|
||||
this(-1, signature, data);
|
||||
super(MessageType.ARBITRARY_DATA);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private ArbitraryDataMessage(int id, byte[] signature, byte[] data) {
|
||||
@@ -35,14 +48,14 @@ public class ArbitraryDataMessage extends Message {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
int dataLength = byteBuffer.getInt();
|
||||
|
||||
if (byteBuffer.remaining() != dataLength)
|
||||
return null;
|
||||
if (byteBuffer.remaining() < dataLength)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
byte[] data = new byte[dataLength];
|
||||
byteBuffer.get(data);
|
||||
@@ -50,24 +63,4 @@ public class ArbitraryDataMessage extends Message {
|
||||
return new ArbitraryDataMessage(id, signature, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
if (this.data == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(this.data.length));
|
||||
|
||||
bytes.write(this.data);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ArbitraryMetadataMessage extends Message {
|
||||
|
||||
private byte[] signature;
|
||||
private ArbitraryDataFile arbitraryMetadataFile;
|
||||
|
||||
public ArbitraryMetadataMessage(byte[] signature, ArbitraryDataFile arbitraryMetadataFile) {
|
||||
super(MessageType.ARBITRARY_METADATA);
|
||||
|
||||
byte[] data = arbitraryMetadataFile.getBytes();
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Ints.toByteArray(data.length));
|
||||
|
||||
bytes.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private ArbitraryMetadataMessage(int id, byte[] signature, ArbitraryDataFile arbitraryMetadataFile) {
|
||||
super(id, MessageType.ARBITRARY_METADATA);
|
||||
|
||||
this.signature = signature;
|
||||
this.arbitraryMetadataFile = arbitraryMetadataFile;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public ArbitraryDataFile getArbitraryMetadataFile() {
|
||||
return this.arbitraryMetadataFile;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
int dataLength = byteBuffer.getInt();
|
||||
|
||||
if (byteBuffer.remaining() < dataLength)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
byte[] data = new byte[dataLength];
|
||||
byteBuffer.get(data);
|
||||
|
||||
try {
|
||||
ArbitraryDataFile arbitraryMetadataFile = new ArbitraryDataFile(data, signature);
|
||||
return new ArbitraryMetadataMessage(id, signature, arbitraryMetadataFile);
|
||||
} catch (DataException e) {
|
||||
throw new MessageException("Unable to process arbitrary metadata message: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,21 +8,37 @@ import org.qortal.utils.Serialization;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ArbitrarySignaturesMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private String peerAddress;
|
||||
private int requestHops;
|
||||
private List<byte[]> signatures;
|
||||
|
||||
public ArbitrarySignaturesMessage(String peerAddress, int requestHops, List<byte[]> signatures) {
|
||||
this(-1, peerAddress, requestHops, signatures);
|
||||
super(MessageType.ARBITRARY_SIGNATURES);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
Serialization.serializeSizedStringV2(bytes, peerAddress);
|
||||
|
||||
bytes.write(Ints.toByteArray(requestHops));
|
||||
|
||||
bytes.write(Ints.toByteArray(signatures.size()));
|
||||
|
||||
for (byte[] signature : signatures)
|
||||
bytes.write(signature);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private ArbitrarySignaturesMessage(int id, String peerAddress, int requestHops, List<byte[]> signatures) {
|
||||
@@ -41,27 +57,24 @@ public class ArbitrarySignaturesMessage extends Message {
|
||||
return this.signatures;
|
||||
}
|
||||
|
||||
public int getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public void setRequestHops(int requestHops) {
|
||||
this.requestHops = requestHops;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
|
||||
String peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
String peerAddress;
|
||||
try {
|
||||
peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
|
||||
} catch (TransformationException e) {
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
int requestHops = bytes.getInt();
|
||||
|
||||
int signatureCount = bytes.getInt();
|
||||
|
||||
if (bytes.remaining() != signatureCount * SIGNATURE_LENGTH)
|
||||
return null;
|
||||
if (bytes.remaining() < signatureCount * Transformer.SIGNATURE_LENGTH)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
List<byte[]> signatures = new ArrayList<>();
|
||||
for (int i = 0; i < signatureCount; ++i) {
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
signatures.add(signature);
|
||||
}
|
||||
@@ -69,24 +82,4 @@ public class ArbitrarySignaturesMessage extends Message {
|
||||
return new ArbitrarySignaturesMessage(id, peerAddress, requestHops, signatures);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
Serialization.serializeSizedStringV2(bytes, this.peerAddress);
|
||||
|
||||
bytes.write(Ints.toByteArray(this.requestHops));
|
||||
|
||||
bytes.write(Ints.toByteArray(this.signatures.size()));
|
||||
|
||||
for (byte[] signature : this.signatures)
|
||||
bytes.write(signature);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -16,27 +12,15 @@ import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
public class BlockMessage extends Message {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockMessage.class);
|
||||
|
||||
private Block block = null;
|
||||
private final BlockData blockData;
|
||||
private final List<TransactionData> transactions;
|
||||
private final List<ATStateData> atStates;
|
||||
|
||||
private BlockData blockData = null;
|
||||
private List<TransactionData> transactions = null;
|
||||
private List<ATStateData> atStates = null;
|
||||
|
||||
private int height;
|
||||
|
||||
public BlockMessage(Block block) {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = block;
|
||||
this.blockData = block.getBlockData();
|
||||
this.height = block.getBlockData().getHeight();
|
||||
}
|
||||
// No public constructor as we're an incoming-only message type.
|
||||
|
||||
private BlockMessage(int id, BlockData blockData, List<TransactionData> transactions, List<ATStateData> atStates) {
|
||||
super(id, MessageType.BLOCK);
|
||||
@@ -44,8 +28,6 @@ public class BlockMessage extends Message {
|
||||
this.blockData = blockData;
|
||||
this.transactions = transactions;
|
||||
this.atStates = atStates;
|
||||
|
||||
this.height = blockData.getHeight();
|
||||
}
|
||||
|
||||
public BlockData getBlockData() {
|
||||
@@ -60,7 +42,7 @@ public class BlockMessage extends Message {
|
||||
return this.atStates;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||
try {
|
||||
int height = byteBuffer.getInt();
|
||||
|
||||
@@ -72,32 +54,8 @@ public class BlockMessage extends Message {
|
||||
return new BlockMessage(id, blockData, blockInfo.getB(), blockInfo.getC());
|
||||
} catch (TransformationException e) {
|
||||
LOGGER.info(String.format("Received garbled BLOCK message: %s", e.getMessage()));
|
||||
return null;
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
if (this.block == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.height));
|
||||
|
||||
bytes.write(BlockTransformer.toBytes(this.block));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (TransformationException | IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BlockMessage cloneWithNewId(int newId) {
|
||||
BlockMessage clone = new BlockMessage(this.block);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -20,7 +20,25 @@ public class BlockSummariesMessage extends Message {
|
||||
private List<BlockSummaryData> blockSummaries;
|
||||
|
||||
public BlockSummariesMessage(List<BlockSummaryData> blockSummaries) {
|
||||
this(-1, blockSummaries);
|
||||
super(MessageType.BLOCK_SUMMARIES);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(blockSummaries.size()));
|
||||
|
||||
for (BlockSummaryData blockSummary : blockSummaries) {
|
||||
bytes.write(Ints.toByteArray(blockSummary.getHeight()));
|
||||
bytes.write(blockSummary.getSignature());
|
||||
bytes.write(blockSummary.getMinterPublicKey());
|
||||
bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private BlockSummariesMessage(int id, List<BlockSummaryData> blockSummaries) {
|
||||
@@ -33,11 +51,11 @@ public class BlockSummariesMessage extends Message {
|
||||
return this.blockSummaries;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
int count = bytes.getInt();
|
||||
|
||||
if (bytes.remaining() != count * BLOCK_SUMMARY_LENGTH)
|
||||
return null;
|
||||
if (bytes.remaining() < count * BLOCK_SUMMARY_LENGTH)
|
||||
throw new BufferUnderflowException();
|
||||
|
||||
List<BlockSummaryData> blockSummaries = new ArrayList<>();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
@@ -58,24 +76,4 @@ public class BlockSummariesMessage extends Message {
|
||||
return new BlockSummariesMessage(id, blockSummaries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.blockSummaries.size()));
|
||||
|
||||
for (BlockSummaryData blockSummary : this.blockSummaries) {
|
||||
bytes.write(Ints.toByteArray(blockSummary.getHeight()));
|
||||
bytes.write(blockSummary.getSignature());
|
||||
bytes.write(blockSummary.getMinterPublicKey());
|
||||
bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.qortal.block.Block;
|
||||
@@ -12,59 +11,34 @@ import org.qortal.transform.block.BlockTransformer;
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
// This is an OUTGOING-only Message which more readily lends itself to being cached
|
||||
public class CachedBlockMessage extends Message {
|
||||
public class CachedBlockMessage extends Message implements Cloneable {
|
||||
|
||||
private Block block = null;
|
||||
private byte[] cachedBytes = null;
|
||||
|
||||
public CachedBlockMessage(Block block) {
|
||||
public CachedBlockMessage(Block block) throws TransformationException {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = block;
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public CachedBlockMessage(byte[] cachedBytes) {
|
||||
super(MessageType.BLOCK);
|
||||
|
||||
this.block = null;
|
||||
this.cachedBytes = cachedBytes;
|
||||
this.dataBytes = cachedBytes;
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
throw new UnsupportedOperationException("CachedBlockMessage is for outgoing messages only");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
// Already serialized?
|
||||
if (this.cachedBytes != null)
|
||||
return cachedBytes;
|
||||
|
||||
if (this.block == null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(Ints.toByteArray(this.block.getBlockData().getHeight()));
|
||||
|
||||
bytes.write(BlockTransformer.toBytes(this.block));
|
||||
|
||||
this.cachedBytes = bytes.toByteArray();
|
||||
// We no longer need source Block
|
||||
// and Block contains repository handle which is highly likely to be invalid after this call
|
||||
this.block = null;
|
||||
|
||||
return this.cachedBytes;
|
||||
} catch (TransformationException | IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public CachedBlockMessage cloneWithNewId(int newId) {
|
||||
CachedBlockMessage clone = new CachedBlockMessage(this.cachedBytes);
|
||||
clone.setId(newId);
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,8 +10,25 @@ public class ChallengeMessage extends Message {
|
||||
|
||||
public static final int CHALLENGE_LENGTH = 32;
|
||||
|
||||
private final byte[] publicKey;
|
||||
private final byte[] challenge;
|
||||
private byte[] publicKey;
|
||||
private byte[] challenge;
|
||||
|
||||
public ChallengeMessage(byte[] publicKey, byte[] challenge) {
|
||||
super(MessageType.CHALLENGE);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(publicKey.length + challenge.length);
|
||||
|
||||
try {
|
||||
bytes.write(publicKey);
|
||||
|
||||
bytes.write(challenge);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private ChallengeMessage(int id, byte[] publicKey, byte[] challenge) {
|
||||
super(id, MessageType.CHALLENGE);
|
||||
@@ -20,10 +37,6 @@ public class ChallengeMessage extends Message {
|
||||
this.challenge = challenge;
|
||||
}
|
||||
|
||||
public ChallengeMessage(byte[] publicKey, byte[] challenge) {
|
||||
this(-1, publicKey, challenge);
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return this.publicKey;
|
||||
}
|
||||
@@ -42,15 +55,4 @@ public class ChallengeMessage extends Message {
|
||||
return new ChallengeMessage(id, publicKey, challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.publicKey);
|
||||
|
||||
bytes.write(this.challenge);
|
||||
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,70 +2,75 @@ package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.network.PeerData;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.Serialization;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.qortal.transform.Transformer.INT_LENGTH;
|
||||
import static org.qortal.transform.Transformer.LONG_LENGTH;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GetArbitraryDataFileListMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private final byte[] signature;
|
||||
private final long requestTime;
|
||||
private byte[] signature;
|
||||
private List<byte[]> hashes;
|
||||
private long requestTime;
|
||||
private int requestHops;
|
||||
private String requestingPeer;
|
||||
|
||||
public GetArbitraryDataFileListMessage(byte[] signature, long requestTime, int requestHops) {
|
||||
this(-1, signature, requestTime, requestHops);
|
||||
public GetArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes, long requestTime, int requestHops, String requestingPeer) {
|
||||
super(MessageType.GET_ARBITRARY_DATA_FILE_LIST);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(requestHops));
|
||||
|
||||
if (hashes != null) {
|
||||
bytes.write(Ints.toByteArray(hashes.size()));
|
||||
|
||||
for (byte[] hash : hashes) {
|
||||
bytes.write(hash);
|
||||
}
|
||||
}
|
||||
else {
|
||||
bytes.write(Ints.toByteArray(0));
|
||||
}
|
||||
|
||||
if (requestingPeer != null) {
|
||||
Serialization.serializeSizedStringV2(bytes, requestingPeer);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetArbitraryDataFileListMessage(int id, byte[] signature, long requestTime, int requestHops) {
|
||||
private GetArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, long requestTime, int requestHops, String requestingPeer) {
|
||||
super(id, MessageType.GET_ARBITRARY_DATA_FILE_LIST);
|
||||
|
||||
this.signature = signature;
|
||||
this.hashes = hashes;
|
||||
this.requestTime = requestTime;
|
||||
this.requestHops = requestHops;
|
||||
this.requestingPeer = requestingPeer;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != SIGNATURE_LENGTH + LONG_LENGTH + INT_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
|
||||
bytes.get(signature);
|
||||
|
||||
long requestTime = bytes.getLong();
|
||||
|
||||
int requestHops = bytes.getInt();
|
||||
|
||||
return new GetArbitraryDataFileListMessage(id, signature, requestTime, requestHops);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(this.requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(this.requestHops));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
public List<byte[]> getHashes() {
|
||||
return this.hashes;
|
||||
}
|
||||
|
||||
public long getRequestTime() {
|
||||
@@ -75,8 +80,42 @@ public class GetArbitraryDataFileListMessage extends Message {
|
||||
public int getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
public void setRequestHops(int requestHops) {
|
||||
this.requestHops = requestHops;
|
||||
|
||||
public String getRequestingPeer() {
|
||||
return this.requestingPeer;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
|
||||
bytes.get(signature);
|
||||
|
||||
long requestTime = bytes.getLong();
|
||||
|
||||
int requestHops = bytes.getInt();
|
||||
|
||||
List<byte[]> hashes = null;
|
||||
if (bytes.hasRemaining()) {
|
||||
int hashCount = bytes.getInt();
|
||||
|
||||
hashes = new ArrayList<>();
|
||||
for (int i = 0; i < hashCount; ++i) {
|
||||
byte[] hash = new byte[Transformer.SHA256_LENGTH];
|
||||
bytes.get(hash);
|
||||
hashes.add(hash);
|
||||
}
|
||||
}
|
||||
|
||||
String requestingPeer = null;
|
||||
if (bytes.hasRemaining()) {
|
||||
try {
|
||||
requestingPeer = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
|
||||
} catch (TransformationException e) {
|
||||
throw new MessageException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return new GetArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops, requestingPeer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetArbitraryDataFileMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
private static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
||||
|
||||
private final byte[] signature;
|
||||
private final byte[] hash;
|
||||
private byte[] signature;
|
||||
private byte[] hash;
|
||||
|
||||
public GetArbitraryDataFileMessage(byte[] signature, byte[] hash) {
|
||||
this(-1, signature, hash);
|
||||
super(MessageType.GET_ARBITRARY_DATA_FILE);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(signature.length + hash.length);
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(hash);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetArbitraryDataFileMessage(int id, byte[] signature, byte[] hash) {
|
||||
@@ -35,32 +43,14 @@ public class GetArbitraryDataFileMessage extends Message {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != HASH_LENGTH + SIGNATURE_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
byte[] hash = new byte[HASH_LENGTH];
|
||||
byte[] hash = new byte[Transformer.SHA256_LENGTH];
|
||||
bytes.get(hash);
|
||||
|
||||
return new GetArbitraryDataFileMessage(id, signature, hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
bytes.write(this.hash);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
public class GetArbitraryDataMessage extends Message {
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private byte[] signature;
|
||||
|
||||
public GetArbitraryDataMessage(byte[] signature) {
|
||||
this(-1, signature);
|
||||
super(MessageType.GET_ARBITRARY_DATA);
|
||||
|
||||
this.dataBytes = Arrays.copyOf(signature, signature.length);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetArbitraryDataMessage(int id, byte[] signature) {
|
||||
@@ -27,28 +26,12 @@ public class GetArbitraryDataMessage extends Message {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != SIGNATURE_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
|
||||
bytes.get(signature);
|
||||
|
||||
return new GetArbitraryDataMessage(id, signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class GetArbitraryMetadataMessage extends Message {
|
||||
|
||||
private byte[] signature;
|
||||
private long requestTime;
|
||||
private int requestHops;
|
||||
|
||||
public GetArbitraryMetadataMessage(byte[] signature, long requestTime, int requestHops) {
|
||||
super(MessageType.GET_ARBITRARY_METADATA);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(signature);
|
||||
|
||||
bytes.write(Longs.toByteArray(requestTime));
|
||||
|
||||
bytes.write(Ints.toByteArray(requestHops));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetArbitraryMetadataMessage(int id, byte[] signature, long requestTime, int requestHops) {
|
||||
super(id, MessageType.GET_ARBITRARY_METADATA);
|
||||
|
||||
this.signature = signature;
|
||||
this.requestTime = requestTime;
|
||||
this.requestHops = requestHops;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public long getRequestTime() {
|
||||
return this.requestTime;
|
||||
}
|
||||
|
||||
public int getRequestHops() {
|
||||
return this.requestHops;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
long requestTime = bytes.getLong();
|
||||
|
||||
int requestHops = bytes.getInt();
|
||||
|
||||
return new GetArbitraryMetadataMessage(id, signature, requestTime, requestHops);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
public class GetBlockMessage extends Message {
|
||||
|
||||
private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
|
||||
|
||||
private byte[] signature;
|
||||
|
||||
public GetBlockMessage(byte[] signature) {
|
||||
this(-1, signature);
|
||||
super(MessageType.GET_BLOCK);
|
||||
|
||||
this.dataBytes = Arrays.copyOf(signature, signature.length);
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
private GetBlockMessage(int id, byte[] signature) {
|
||||
@@ -27,28 +26,11 @@ public class GetBlockMessage extends Message {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH)
|
||||
return null;
|
||||
|
||||
byte[] signature = new byte[BLOCK_SIGNATURE_LENGTH];
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
return new GetBlockMessage(id, signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] toData() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
bytes.write(this.signature);
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user