forked from Qortal/qortal
Compare commits
83 Commits
block-mint
...
hosted-res
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7740f3da7e | ||
|
|
badb576991 | ||
|
|
c65a63fc7e | ||
|
|
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 | ||
|
|
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 | ||
|
|
214f49e356 | ||
|
|
d7658ee9f9 | ||
|
|
70c864bc2f | ||
|
|
9804eccbf0 | ||
|
|
d1f24d45da | ||
|
|
9630625449 | ||
|
|
b72153f62b | ||
|
|
0a88a0c95e | ||
|
|
ab4ba9bb17 | ||
|
|
a49218a840 | ||
|
|
b6d633ab24 | ||
|
|
133943cd4e | ||
|
|
f8ffb1a179 | ||
|
|
244d4f78e2 |
@@ -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:{9BDE0BDF-72A2-44DA-8B55-E7C129DBE603} 1049:{F4FCC1D9-D286-4B3D-A50F-82034010A30F} 2052:{DBE9D682-F666-49BA-8B63-28C0AE06CBCA} 2057:{949F4DFE-E55C-4493-AAB6-5DB13E68C754} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{5FC8DCC3-BF9C-4D72-8C6D-940340ACD1B8} 1049:{1DEF14AB-2397-4517-B3C8-13221B921753} 2052:{B9E3C1DF-C92D-440A-9A21-869582F8585F} 2057:{91D69E7B-CA7D-4449-8E8A-F22DCEA546FC} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="3.0.4" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="3.1.1" 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="{9B71A82D-8C25-40FD-806D-44BAD0B45AA2}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{42F5EC19-E46F-4299-B9F7-6E1112F6E4FB}" 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"/>
|
||||
|
||||
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>
|
||||
9
pom.xml
9
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.1.1</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
@@ -21,6 +21,7 @@
|
||||
<dagger.version>1.2.2</dagger.version>
|
||||
<guava.version>28.1-jre</guava.version>
|
||||
<hsqldb.version>2.5.1</hsqldb.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 +428,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>
|
||||
|
||||
@@ -272,7 +272,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 +288,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -198,7 +198,7 @@ public class AddressesResource {
|
||||
|
||||
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)
|
||||
|
||||
@@ -315,6 +315,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) {
|
||||
@@ -355,6 +356,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) {
|
||||
@@ -546,7 +548,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")
|
||||
|
||||
@@ -432,14 +432,23 @@ public class ArbitraryResource {
|
||||
@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@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();
|
||||
@@ -461,6 +470,8 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@DELETE
|
||||
@Path("/resource/{service}/{name}/{identifier}")
|
||||
@Operation(
|
||||
@@ -1044,7 +1055,7 @@ public class ArbitraryResource {
|
||||
// Loop until we have data
|
||||
if (async) {
|
||||
// Asynchronous
|
||||
arbitraryDataReader.loadAsynchronously(false);
|
||||
arbitraryDataReader.loadAsynchronously(false, 1);
|
||||
}
|
||||
else {
|
||||
// Synchronous
|
||||
|
||||
@@ -354,7 +354,7 @@ public class PeersResource {
|
||||
|
||||
List<Peer> connectedPeers = Network.getInstance().getConnectedPeers().stream().collect(Collectors.toList());
|
||||
for (Peer peer : connectedPeers) {
|
||||
if (peer.isOutbound()) {
|
||||
if (!peer.isOutbound()) {
|
||||
peersSummary.inboundConnections++;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -366,6 +366,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;
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ public class ArbitraryDataReader {
|
||||
* @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(boolean overwrite) {
|
||||
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()) {
|
||||
@@ -135,7 +135,9 @@ public class ArbitraryDataReader {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ArbitraryDataBuildManager.getInstance().addToBuildQueue(this.createQueueItem());
|
||||
ArbitraryDataBuildQueueItem item = this.createQueueItem();
|
||||
item.setPriority(priority);
|
||||
return ArbitraryDataBuildManager.getInstance().addToBuildQueue(item);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -468,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ public class ArbitraryDataRenderer {
|
||||
if (!arbitraryDataReader.isCachedDataAvailable()) {
|
||||
// If async is requested, show a loading screen whilst build is in progress
|
||||
if (async) {
|
||||
arbitraryDataReader.loadAsynchronously(false);
|
||||
arbitraryDataReader.loadAsynchronously(false, 10);
|
||||
return this.getLoadingResponse(service, resourceId);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,11 @@ public class BlockChain {
|
||||
transactionV5Timestamp;
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private long nameRegistrationUnitFee;
|
||||
private long nameRegistrationUnitFeeTimestamp;
|
||||
|
||||
/** Map of which blockchain features are enabled when (height/timestamp) */
|
||||
@XmlJavaTypeAdapter(StringLongMapXmlAdapter.class)
|
||||
private Map<String, Long> featureTriggers;
|
||||
@@ -301,6 +306,16 @@ public class BlockChain {
|
||||
return this.maxBlockSize;
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
public long getNameRegistrationUnitFee() {
|
||||
return this.nameRegistrationUnitFee;
|
||||
}
|
||||
|
||||
public long getNameRegistrationUnitFeeTimestamp() {
|
||||
// FUTURE: we could use a separate structure to indicate fee adjustments for different transaction types
|
||||
return this.nameRegistrationUnitFeeTimestamp;
|
||||
}
|
||||
|
||||
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
||||
public boolean getRequireGroupForApproval() {
|
||||
return this.requireGroupForApproval;
|
||||
|
||||
@@ -343,6 +343,9 @@ public class BlockMinter extends Thread {
|
||||
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;
|
||||
|
||||
@@ -105,6 +105,8 @@ public class Controller extends Thread {
|
||||
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 static volatile boolean isStopping = false;
|
||||
private static BlockMinter blockMinter = null;
|
||||
@@ -515,7 +517,7 @@ public class Controller extends Thread {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Controller");
|
||||
Thread.currentThread().setName("Qortal");
|
||||
|
||||
final long repositoryBackupInterval = Settings.getInstance().getRepositoryBackupInterval();
|
||||
final long repositoryCheckpointInterval = Settings.getInstance().getRepositoryCheckpointInterval();
|
||||
@@ -765,14 +767,14 @@ public class Controller extends Thread {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||
SysTray.getInstance().setTrayIcon(2);
|
||||
}
|
||||
else if (Synchronizer.getInstance().isSynchronizing()) {
|
||||
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), Synchronizer.getInstance().getSyncPercent());
|
||||
SysTray.getInstance().setTrayIcon(3);
|
||||
}
|
||||
else if (numberOfPeers < Settings.getInstance().getMinBlockchainPeers()) {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "CONNECTING");
|
||||
SysTray.getInstance().setTrayIcon(3);
|
||||
}
|
||||
else if (!this.isUpToDate()) {
|
||||
actionText = String.format("%s - %d%%", Translator.INSTANCE.translate("SysTray", "SYNCHRONIZING_BLOCKCHAIN"), Synchronizer.getInstance().getSyncPercent());
|
||||
SysTray.getInstance().setTrayIcon(3);
|
||||
}
|
||||
else {
|
||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_DISABLED");
|
||||
SysTray.getInstance().setTrayIcon(4);
|
||||
@@ -824,6 +826,16 @@ public class Controller extends Thread {
|
||||
|
||||
// Incoming transactions queue
|
||||
|
||||
private boolean incomingTransactionQueueContains(byte[] signature) {
|
||||
synchronized (incomingTransactions) {
|
||||
return incomingTransactions.stream().anyMatch(t -> Arrays.equals(t.getSignature(), signature));
|
||||
}
|
||||
}
|
||||
|
||||
private void removeIncomingTransaction(byte[] signature) {
|
||||
incomingTransactions.removeIf(t -> Arrays.equals(t.getSignature(), signature));
|
||||
}
|
||||
|
||||
private void processIncomingTransactionsQueue() {
|
||||
if (this.incomingTransactions.size() == 0) {
|
||||
// Don't bother locking if there are no new transactions to process
|
||||
@@ -847,65 +859,73 @@ public class Controller extends Thread {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
LOGGER.debug("Processing incoming transactions queue (size {})...", this.incomingTransactions.size());
|
||||
|
||||
// Take a copy of incomingTransactions so we can release the lock
|
||||
List<TransactionData>incomingTransactionsCopy = new ArrayList<>(this.incomingTransactions);
|
||||
|
||||
// Iterate through incoming transactions list
|
||||
synchronized (this.incomingTransactions) { // Required in order to safely iterate a synchronizedList()
|
||||
Iterator iterator = this.incomingTransactions.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionData transactionData = (TransactionData) iterator.next();
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
// Check signature
|
||||
if (!transaction.isSignatureValid()) {
|
||||
LOGGER.trace(() -> String.format("Ignoring %s transaction %s with invalid signature", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
ValidationResult validationResult = transaction.importAsUnconfirmed();
|
||||
|
||||
if (validationResult == ValidationResult.TRANSACTION_ALREADY_EXISTS) {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (validationResult == ValidationResult.NO_BLOCKCHAIN_LOCK) {
|
||||
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction", Base58.encode(transactionData.getSignature())));
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (validationResult != ValidationResult.OK) {
|
||||
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 == ValidationResult.TIMESTAMP_TOO_OLD) {
|
||||
// Use shorter recheck interval for expired transactions
|
||||
expiryLength = EXPIRED_TRANSACTION_RECHECK_INTERVAL;
|
||||
}
|
||||
Long expiry = now + expiryLength;
|
||||
LOGGER.debug("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);
|
||||
}
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
|
||||
iterator.remove();
|
||||
Iterator iterator = incomingTransactionsCopy.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Synchronizer.getInstance().isSyncRequestPending()) {
|
||||
LOGGER.debug("Breaking out of transaction processing loop with {} remaining, because a sync request is pending", incomingTransactionsCopy.size());
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionData transactionData = (TransactionData) iterator.next();
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
// Check signature
|
||||
if (!transaction.isSignatureValid()) {
|
||||
LOGGER.trace(() -> String.format("Ignoring %s transaction %s with invalid signature", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
continue;
|
||||
}
|
||||
|
||||
ValidationResult validationResult = transaction.importAsUnconfirmed();
|
||||
|
||||
if (validationResult == ValidationResult.TRANSACTION_ALREADY_EXISTS) {
|
||||
LOGGER.trace(() -> String.format("Ignoring existing transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (validationResult == ValidationResult.NO_BLOCKCHAIN_LOCK) {
|
||||
LOGGER.trace(() -> String.format("Couldn't lock blockchain to import unconfirmed transaction", Base58.encode(transactionData.getSignature())));
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (validationResult != ValidationResult.OK) {
|
||||
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 == ValidationResult.TIMESTAMP_TOO_OLD) {
|
||||
// Use shorter recheck interval for expired transactions
|
||||
expiryLength = EXPIRED_TRANSACTION_RECHECK_INTERVAL;
|
||||
}
|
||||
Long expiry = now + expiryLength;
|
||||
LOGGER.debug("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);
|
||||
}
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.debug(() -> String.format("Imported %s transaction %s", transactionData.getType().name(), Base58.encode(transactionData.getSignature())));
|
||||
removeIncomingTransaction(transactionData.getSignature());
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error(String.format("Repository issue while processing incoming transactions", e));
|
||||
} finally {
|
||||
LOGGER.debug("Finished processing incoming transactions queue");
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
}
|
||||
@@ -1264,6 +1284,14 @@ public class Controller extends Thread {
|
||||
onNetworkOnlineAccountsMessage(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ONLINE_ACCOUNTS_V2:
|
||||
onNetworkGetOnlineAccountsV2Message(peer, message);
|
||||
break;
|
||||
|
||||
case ONLINE_ACCOUNTS_V2:
|
||||
onNetworkOnlineAccountsV2Message(peer, message);
|
||||
break;
|
||||
|
||||
case GET_ARBITRARY_DATA:
|
||||
// Not currently supported
|
||||
break;
|
||||
@@ -1594,6 +1622,12 @@ public class Controller extends Thread {
|
||||
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));
|
||||
@@ -1663,6 +1697,53 @@ public class Controller extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private 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));
|
||||
}
|
||||
|
||||
private void onNetworkOnlineAccountsV2Message(Peer peer, Message message) {
|
||||
OnlineAccountsV2Message onlineAccountsMessage = (OnlineAccountsV2Message) 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
private void verifyAndAddAccount(Repository repository, OnlineAccountData onlineAccountData) throws DataException {
|
||||
@@ -1774,11 +1855,17 @@ public class Controller extends Thread {
|
||||
|
||||
// Request data from other peers?
|
||||
if ((this.onlineAccountsTasksTimestamp % ONLINE_ACCOUNTS_BROADCAST_INTERVAL) < ONLINE_ACCOUNTS_TASKS_INTERVAL) {
|
||||
Message message;
|
||||
List<OnlineAccountData> safeOnlineAccounts;
|
||||
synchronized (this.onlineAccounts) {
|
||||
message = new GetOnlineAccountsMessage(this.onlineAccounts);
|
||||
safeOnlineAccounts = new ArrayList<>(this.onlineAccounts);
|
||||
}
|
||||
Network.getInstance().broadcast(peer -> message);
|
||||
|
||||
Message messageV1 = new GetOnlineAccountsMessage(safeOnlineAccounts);
|
||||
Message messageV2 = new GetOnlineAccountsV2Message(safeOnlineAccounts);
|
||||
|
||||
Network.getInstance().broadcast(peer ->
|
||||
peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION ? messageV2 : messageV1
|
||||
);
|
||||
}
|
||||
|
||||
// Refresh our online accounts signatures?
|
||||
@@ -1870,8 +1957,12 @@ public class Controller extends Thread {
|
||||
if (!hasInfoChanged)
|
||||
return;
|
||||
|
||||
Message message = new OnlineAccountsMessage(ourOnlineAccounts);
|
||||
Network.getInstance().broadcast(peer -> message);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ public class Synchronizer extends Thread {
|
||||
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<>());
|
||||
@@ -111,6 +112,8 @@ public class Synchronizer extends Thread {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Synchronizer");
|
||||
|
||||
try {
|
||||
while (running && !Controller.isStopping()) {
|
||||
Thread.sleep(1000);
|
||||
@@ -122,6 +125,8 @@ public class Synchronizer extends Thread {
|
||||
// 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) {
|
||||
@@ -142,6 +147,10 @@ public class Synchronizer extends Thread {
|
||||
return this.isSynchronizing;
|
||||
}
|
||||
|
||||
public boolean isSyncRequestPending() {
|
||||
return this.syncRequestPending;
|
||||
}
|
||||
|
||||
public Integer getSyncPercent() {
|
||||
synchronized (this.syncLock) {
|
||||
return this.isSynchronizing ? this.syncPercent : null;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -27,7 +28,7 @@ public class ArbitraryDataBuilderThread implements Runnable {
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
Thread.sleep(100);
|
||||
|
||||
if (buildManager.arbitraryDataBuildQueue == null) {
|
||||
continue;
|
||||
@@ -36,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().orElse(null);
|
||||
|
||||
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);
|
||||
@@ -96,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,18 +266,16 @@ public class ArbitraryDataFileListManager {
|
||||
List<Peer> handshakedPeers = Network.getInstance().getHandshakedPeers();
|
||||
List<byte[]> missingHashes = null;
|
||||
|
||||
// // TODO: uncomment after GetArbitraryDataFileListMessage updates are deployed
|
||||
// // 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;
|
||||
// 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;
|
||||
|
||||
int hashCount = 0;
|
||||
LOGGER.debug(String.format("Sending data file list request for signature %s with %d hashes to %d peers...", signature58, hashCount, handshakedPeers.size()));
|
||||
|
||||
// Build request
|
||||
|
||||
@@ -37,7 +37,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
/**
|
||||
* 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
|
||||
@@ -148,7 +148,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -240,16 +240,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,16 +393,16 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
}
|
||||
|
||||
private ArbitraryRelayInfo getRandomRelayInfoEntryForHash(String hash58) {
|
||||
LOGGER.info("Fetching random relay info for hash: {}", 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.info("Returning random relay info for hash: {} (index {})", hash58, index);
|
||||
LOGGER.trace("Returning random relay info for hash: {} (index {})", hash58, index);
|
||||
return relayInfoList.get(index);
|
||||
}
|
||||
LOGGER.info("No relay info exists for hash: {}", hash58);
|
||||
LOGGER.trace("No relay info exists for hash: {}", hash58);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ public class ArbitraryDataFileRequestThread implements Runnable {
|
||||
|
||||
try {
|
||||
while (!Controller.isStopping()) {
|
||||
Thread.sleep(1000);
|
||||
|
||||
Long now = NTP.getTime();
|
||||
this.processFileHashes(now);
|
||||
}
|
||||
@@ -41,67 +39,72 @@ public class ArbitraryDataFileRequestThread implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void processFileHashes(Long now) {
|
||||
if (Controller.isStopping()) {
|
||||
private void processFileHashes(Long now) throws InterruptedException {
|
||||
if (Controller.isStopping()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance();
|
||||
ArbitraryDataFileManager arbitraryDataFileManager = ArbitraryDataFileManager.getInstance();
|
||||
String signature58 = null;
|
||||
String hash58 = null;
|
||||
Peer peer = null;
|
||||
boolean shouldProcess = false;
|
||||
|
||||
ArbitraryTransactionData arbitraryTransactionData = null;
|
||||
byte[] signature = null;
|
||||
byte[] hash = null;
|
||||
Peer peer = null;
|
||||
boolean shouldProcess = false;
|
||||
|
||||
synchronized (arbitraryDataFileManager.arbitraryDataFileHashResponses) {
|
||||
Iterator iterator = arbitraryDataFileManager.arbitraryDataFileHashResponses.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (Controller.isStopping()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map.Entry entry = (Map.Entry) iterator.next();
|
||||
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
String hash58 = (String) entry.getKey();
|
||||
Triple<Peer, String, Long> value = (Triple<Peer, String, Long>) entry.getValue();
|
||||
if (value == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
peer = value.getA();
|
||||
String signature58 = value.getB();
|
||||
Long timestamp = value.getC();
|
||||
|
||||
if (now - timestamp >= ArbitraryDataManager.ARBITRARY_RELAY_TIMEOUT || signature58 == null || peer == null) {
|
||||
// Ignore - to be deleted
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
hash = Base58.decode(hash58);
|
||||
signature = Base58.decode(signature58);
|
||||
|
||||
// We want to process this file
|
||||
shouldProcess = true;
|
||||
iterator.remove();
|
||||
break;
|
||||
synchronized (arbitraryDataFileManager.arbitraryDataFileHashResponses) {
|
||||
Iterator iterator = arbitraryDataFileManager.arbitraryDataFileHashResponses.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (Controller.isStopping()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldProcess) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
Map.Entry entry = (Map.Entry) iterator.next();
|
||||
if (entry == null || entry.getKey() == null || entry.getValue() == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch the transaction data
|
||||
arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
hash58 = (String) entry.getKey();
|
||||
Triple<Peer, String, Long> value = (Triple<Peer, String, Long>) entry.getValue();
|
||||
if (value == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
peer = value.getA();
|
||||
signature58 = value.getB();
|
||||
Long timestamp = value.getC();
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -110,7 +113,6 @@ public class ArbitraryDataFileRequestThread implements Runnable {
|
||||
return;
|
||||
}
|
||||
|
||||
String hash58 = Base58.encode(hash);
|
||||
LOGGER.debug("Fetching file {} from peer {} via request thread...", hash58, peer);
|
||||
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, Arrays.asList(hash));
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ 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 = 60 * 1000L; // ms
|
||||
@@ -80,6 +80,9 @@ public class ArbitraryDataManager extends Thread {
|
||||
Thread.currentThread().setName("Arbitrary Data Manager");
|
||||
|
||||
try {
|
||||
// Wait for node to finish starting up and making connections
|
||||
Thread.sleep(2 * 60 * 1000L);
|
||||
|
||||
while (!isStopping) {
|
||||
Thread.sleep(2000);
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -257,14 +261,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
|
||||
@@ -295,10 +293,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 +379,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,14 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
* 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;
|
||||
|
||||
@@ -229,6 +234,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>
|
||||
@@ -263,7 +287,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,7 +409,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
Set<String> keySet = new HashSet<>();
|
||||
|
||||
// Set the number of consecutive empty batches required before giving up
|
||||
final int numberOfAdditionalBatchesToSearch = 5;
|
||||
final int numberOfAdditionalBatchesToSearch = 7;
|
||||
|
||||
int unusedCounter = 0;
|
||||
int ki = 0;
|
||||
@@ -391,7 +425,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
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);
|
||||
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
areAllKeysUnused = false;
|
||||
@@ -441,19 +475,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);
|
||||
@@ -461,22 +503,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);
|
||||
}
|
||||
|
||||
@@ -569,7 +628,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -539,6 +550,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 +650,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 +661,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 +676,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");
|
||||
|
||||
@@ -50,8 +50,12 @@ public class Litecoin extends Bitcoiny {
|
||||
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("electrum2.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("electrum3.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("electrum3.cipig.net", ConnectionType.TCP, 10063),
|
||||
new Server("electrum2.cipig.net", Server.ConnectionType.TCP, 10063),
|
||||
new Server("electrum1.cipig.net", Server.ConnectionType.SSL, 20063),
|
||||
new Server("electrum1.cipig.net", Server.ConnectionType.TCP, 10063),
|
||||
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
||||
new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
|
||||
new Server("electrum-ltc-bysh.me", Server.ConnectionType.TCP, 50002),
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
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;
|
||||
@@ -7,7 +8,6 @@ import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.network.PeerData;
|
||||
@@ -183,6 +183,14 @@ 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();
|
||||
}
|
||||
@@ -243,12 +251,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,10 +274,13 @@ 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 = null;
|
||||
synchronized (this.connectedPeers) {
|
||||
connectedPeer = this.connectedPeers.stream()
|
||||
.filter(p -> p.getPeerData().getAddress().equals(peerAddress))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
boolean isConnected = (connectedPeer != null);
|
||||
|
||||
boolean isHandshaked = this.getHandshakedPeers().stream()
|
||||
@@ -1178,7 +1192,7 @@ public class Network {
|
||||
public void onExternalIpUpdate(String ipAddress) {
|
||||
LOGGER.info("External IP address updated to {}", ipAddress);
|
||||
|
||||
ArbitraryDataManager.getInstance().broadcastHostedSignatureList();
|
||||
//ArbitraryDataManager.getInstance().broadcastHostedSignatureList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
@@ -12,6 +14,8 @@ import java.nio.ByteBuffer;
|
||||
|
||||
public class ArbitraryDataFileMessage extends Message {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileMessage.class);
|
||||
|
||||
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||
|
||||
private final byte[] signature;
|
||||
@@ -52,6 +56,7 @@ public class ArbitraryDataFileMessage extends Message {
|
||||
return new ArbitraryDataFileMessage(id, signature, arbitraryDataFile);
|
||||
}
|
||||
catch (DataException e) {
|
||||
LOGGER.info("Unable to process received file: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* For requesting online accounts info from remote peer, given our list of online accounts.
|
||||
*
|
||||
* Different format to V1:
|
||||
* V1 is: number of entries, then timestamp + pubkey for each entry
|
||||
* V2 is: groups of: number of entries, timestamp, then pubkey for each entry
|
||||
*
|
||||
* Also V2 only builds online accounts message once!
|
||||
*/
|
||||
public class GetOnlineAccountsV2Message extends Message {
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
private byte[] cachedData;
|
||||
|
||||
public GetOnlineAccountsV2Message(List<OnlineAccountData> onlineAccounts) {
|
||||
this(-1, onlineAccounts);
|
||||
}
|
||||
|
||||
private GetOnlineAccountsV2Message(int id, List<OnlineAccountData> onlineAccounts) {
|
||||
super(id, MessageType.GET_ONLINE_ACCOUNTS_V2);
|
||||
|
||||
this.onlineAccounts = onlineAccounts;
|
||||
}
|
||||
|
||||
public List<OnlineAccountData> getOnlineAccounts() {
|
||||
return this.onlineAccounts;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
int accountCount = bytes.getInt();
|
||||
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
|
||||
|
||||
while (accountCount > 0) {
|
||||
long timestamp = bytes.getLong();
|
||||
|
||||
for (int i = 0; i < accountCount; ++i) {
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
bytes.get(publicKey);
|
||||
|
||||
onlineAccounts.add(new OnlineAccountData(timestamp, null, publicKey));
|
||||
}
|
||||
|
||||
if (bytes.hasRemaining()) {
|
||||
accountCount = bytes.getInt();
|
||||
} else {
|
||||
// we've finished
|
||||
accountCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return new GetOnlineAccountsV2Message(id, onlineAccounts);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized byte[] toData() {
|
||||
if (this.cachedData != null)
|
||||
return this.cachedData;
|
||||
|
||||
// Shortcut in case we have no online accounts
|
||||
if (this.onlineAccounts.isEmpty()) {
|
||||
this.cachedData = Ints.toByteArray(0);
|
||||
return this.cachedData;
|
||||
}
|
||||
|
||||
// How many of each timestamp
|
||||
Map<Long, Integer> countByTimestamp = new HashMap<>();
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
Long timestamp = onlineAccountData.getTimestamp();
|
||||
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
|
||||
+ this.onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH;
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
for (long timestamp : countByTimestamp.keySet()) {
|
||||
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
|
||||
if (onlineAccountData.getTimestamp() == timestamp)
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedData = bytes.toByteArray();
|
||||
return this.cachedData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -78,6 +78,8 @@ public abstract class Message {
|
||||
|
||||
ONLINE_ACCOUNTS(80),
|
||||
GET_ONLINE_ACCOUNTS(81),
|
||||
ONLINE_ACCOUNTS_V2(82),
|
||||
GET_ONLINE_ACCOUNTS_V2(83),
|
||||
|
||||
ARBITRARY_DATA(90),
|
||||
GET_ARBITRARY_DATA(91),
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* For sending online accounts info to remote peer.
|
||||
*
|
||||
* Different format to V1:
|
||||
* V1 is: number of entries, then timestamp + sig + pubkey for each entry
|
||||
* V2 is: groups of: number of entries, timestamp, then sig + pubkey for each entry
|
||||
*
|
||||
* Also V2 only builds online accounts message once!
|
||||
*/
|
||||
public class OnlineAccountsV2Message extends Message {
|
||||
private List<OnlineAccountData> onlineAccounts;
|
||||
private byte[] cachedData;
|
||||
|
||||
public OnlineAccountsV2Message(List<OnlineAccountData> onlineAccounts) {
|
||||
this(-1, onlineAccounts);
|
||||
}
|
||||
|
||||
private OnlineAccountsV2Message(int id, List<OnlineAccountData> onlineAccounts) {
|
||||
super(id, MessageType.ONLINE_ACCOUNTS_V2);
|
||||
|
||||
this.onlineAccounts = onlineAccounts;
|
||||
}
|
||||
|
||||
public List<OnlineAccountData> getOnlineAccounts() {
|
||||
return this.onlineAccounts;
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
|
||||
int accountCount = bytes.getInt();
|
||||
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
|
||||
|
||||
while (accountCount > 0) {
|
||||
long timestamp = bytes.getLong();
|
||||
|
||||
for (int i = 0; i < accountCount; ++i) {
|
||||
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
bytes.get(signature);
|
||||
|
||||
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
bytes.get(publicKey);
|
||||
|
||||
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey));
|
||||
}
|
||||
|
||||
if (bytes.hasRemaining()) {
|
||||
accountCount = bytes.getInt();
|
||||
} else {
|
||||
// we've finished
|
||||
accountCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return new OnlineAccountsV2Message(id, onlineAccounts);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized byte[] toData() {
|
||||
if (this.cachedData != null)
|
||||
return this.cachedData;
|
||||
|
||||
// Shortcut in case we have no online accounts
|
||||
if (this.onlineAccounts.isEmpty()) {
|
||||
this.cachedData = Ints.toByteArray(0);
|
||||
return this.cachedData;
|
||||
}
|
||||
|
||||
// How many of each timestamp
|
||||
Map<Long, Integer> countByTimestamp = new HashMap<>();
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
Long timestamp = onlineAccountData.getTimestamp();
|
||||
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
|
||||
}
|
||||
|
||||
// We should know exactly how many bytes to allocate now
|
||||
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
|
||||
+ this.onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH);
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
|
||||
|
||||
for (long timestamp : countByTimestamp.keySet()) {
|
||||
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
|
||||
|
||||
bytes.write(Longs.toByteArray(timestamp));
|
||||
|
||||
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
|
||||
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
|
||||
|
||||
if (onlineAccountData.getTimestamp() == timestamp) {
|
||||
bytes.write(onlineAccountData.getSignature());
|
||||
|
||||
bytes.write(onlineAccountData.getPublicKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedData = bytes.toByteArray();
|
||||
return this.cachedData;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -181,6 +181,8 @@ public class Settings {
|
||||
private boolean isTestNet = false;
|
||||
/** Port number for inbound peer-to-peer connections. */
|
||||
private Integer listenPort;
|
||||
/** Whether to attempt to open the listen port via UPnP */
|
||||
private boolean uPnPEnabled = true;
|
||||
/** Minimum number of peers to allow block minting / synchronization. */
|
||||
private int minBlockchainPeers = 5;
|
||||
/** Target number of outbound connections to peers we should make. */
|
||||
@@ -195,7 +197,7 @@ public class Settings {
|
||||
private int maxRetries = 2;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "3.0.1";
|
||||
private String minPeerVersion = "3.1.0";
|
||||
/** Whether to allow connections with peers below minPeerVersion
|
||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||
@@ -243,7 +245,8 @@ public class Settings {
|
||||
private String[] bootstrapHosts = new String[] {
|
||||
"http://bootstrap.qortal.org",
|
||||
"http://bootstrap2.qortal.org",
|
||||
"http://cinfu1.crowetic.com"
|
||||
"http://81.169.136.59",
|
||||
"http://62.171.190.193"
|
||||
};
|
||||
|
||||
// Auto-update sources
|
||||
@@ -628,6 +631,10 @@ public class Settings {
|
||||
return this.bindAddress;
|
||||
}
|
||||
|
||||
public boolean isUPnPEnabled() {
|
||||
return this.uPnPEnabled;
|
||||
}
|
||||
|
||||
public int getMinBlockchainPeers() {
|
||||
return this.minBlockchainPeers;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,15 @@ public class RegisterNameTransaction extends Transaction {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUnitFee(Long timestamp) {
|
||||
// Use a higher unit fee after the fee increase timestamp
|
||||
if (timestamp > BlockChain.getInstance().getNameRegistrationUnitFeeTimestamp()) {
|
||||
return BlockChain.getInstance().getNameRegistrationUnitFee();
|
||||
}
|
||||
return BlockChain.getInstance().getUnitFee();
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
||||
public Account getRegistrant() {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.001",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"nameRegistrationUnitFeeTimestamp": 1645372800000,
|
||||
"useBrokenMD160ForAddresses": false,
|
||||
"requireGroupForApproval": false,
|
||||
"defaultGroupId": 0,
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.qortal.test.common.ApiCommon;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class NamesApiTests extends ApiCommon {
|
||||
|
||||
@@ -47,6 +49,7 @@ public class NamesApiTests extends ApiCommon {
|
||||
String name = "test-name";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
assertNotNull(this.namesResource.getNamesByAddress(alice.getAddress(), null, null, null));
|
||||
@@ -62,6 +65,7 @@ public class NamesApiTests extends ApiCommon {
|
||||
String name = "test-name";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
assertNotNull(this.namesResource.getName(name));
|
||||
@@ -77,6 +81,7 @@ public class NamesApiTests extends ApiCommon {
|
||||
long price = 1_23456789L;
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Sell-name
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.qortal.test.common.ArbitraryUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
@@ -153,6 +154,7 @@ public class ArbitraryDataStorageCapacityTests extends Common {
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String aliceName = "alice";
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), aliceName, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
Path alicePath = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
ArbitraryDataFile aliceArbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, Base58.encode(alice.getPublicKey()), alicePath, aliceName, identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize);
|
||||
@@ -161,6 +163,7 @@ public class ArbitraryDataStorageCapacityTests extends Common {
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
String bobName = "bob";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), bobName, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, bob);
|
||||
Path bobPath = ArbitraryUtils.generateRandomDataPath(dataLength);
|
||||
ArbitraryDataFile bobArbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, Base58.encode(bob.getPublicKey()), bobPath, bobName, identifier, ArbitraryTransactionData.Method.PUT, service, bob, chunkSize);
|
||||
|
||||
@@ -21,7 +21,9 @@ import org.qortal.settings.Settings;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -59,25 +61,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
||||
String name = "Test";
|
||||
|
||||
// Register the name to Alice
|
||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create transaction
|
||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
||||
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||
|
||||
// Add name to followed list
|
||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||
|
||||
// We should store and pre-fetch data for this transaction
|
||||
assertEquals(StoragePolicy.FOLLOWED_OR_VIEWED, Settings.getInstance().getStoragePolicy());
|
||||
assertTrue(storageManager.canStoreData(transactionData));
|
||||
assertTrue(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
|
||||
// Now unfollow the name
|
||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||
|
||||
// We should store but not pre-fetch data for this transaction
|
||||
assertTrue(storageManager.canStoreData(transactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,25 +96,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
||||
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "FOLLOWED", true);
|
||||
|
||||
// Register the name to Alice
|
||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create transaction
|
||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
||||
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||
|
||||
// Add name to followed list
|
||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||
|
||||
// We should store and pre-fetch data for this transaction
|
||||
assertEquals(StoragePolicy.FOLLOWED, Settings.getInstance().getStoragePolicy());
|
||||
assertTrue(storageManager.canStoreData(transactionData));
|
||||
assertTrue(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
|
||||
// Now unfollow the name
|
||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||
|
||||
// We shouldn't store or pre-fetch data for this transaction
|
||||
assertFalse(storageManager.canStoreData(transactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertFalse(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,25 +131,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
||||
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "VIEWED", true);
|
||||
|
||||
// Register the name to Alice
|
||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create transaction
|
||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
||||
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||
|
||||
// Add name to followed list
|
||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||
|
||||
// We should store but not pre-fetch data for this transaction
|
||||
assertEquals(StoragePolicy.VIEWED, Settings.getInstance().getStoragePolicy());
|
||||
assertTrue(storageManager.canStoreData(transactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
|
||||
// Now unfollow the name
|
||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||
|
||||
// We should store but not pre-fetch data for this transaction
|
||||
assertTrue(storageManager.canStoreData(transactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,25 +166,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
||||
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "ALL", true);
|
||||
|
||||
// Register the name to Alice
|
||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create transaction
|
||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
||||
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||
|
||||
// Add name to followed list
|
||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||
|
||||
// We should store and pre-fetch data for this transaction
|
||||
assertEquals(StoragePolicy.ALL, Settings.getInstance().getStoragePolicy());
|
||||
assertTrue(storageManager.canStoreData(transactionData));
|
||||
assertTrue(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
|
||||
// Now unfollow the name
|
||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||
|
||||
// We should store and pre-fetch data for this transaction
|
||||
assertTrue(storageManager.canStoreData(transactionData));
|
||||
assertTrue(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertTrue(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertTrue(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,25 +201,27 @@ public class ArbitraryDataStoragePolicyTests extends Common {
|
||||
FieldUtils.writeField(Settings.getInstance(), "storagePolicy", "NONE", true);
|
||||
|
||||
// Register the name to Alice
|
||||
TransactionUtils.signAndMint(repository, new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""), alice);
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create transaction
|
||||
ArbitraryTransactionData transactionData = this.createTxnWithName(repository, alice, name);
|
||||
ArbitraryTransactionData arbitraryTransactionData = this.createTxnWithName(repository, alice, name);
|
||||
|
||||
// Add name to followed list
|
||||
assertTrue(ResourceListManager.getInstance().addToList("followedNames", name, false));
|
||||
|
||||
// We shouldn't store or pre-fetch data for this transaction
|
||||
assertEquals(StoragePolicy.NONE, Settings.getInstance().getStoragePolicy());
|
||||
assertFalse(storageManager.canStoreData(transactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertFalse(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
|
||||
// Now unfollow the name
|
||||
assertTrue(ResourceListManager.getInstance().removeFromList("followedNames", name, false));
|
||||
|
||||
// We shouldn't store or pre-fetch data for this transaction
|
||||
assertFalse(storageManager.canStoreData(transactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, transactionData));
|
||||
assertFalse(storageManager.canStoreData(arbitraryTransactionData));
|
||||
assertFalse(storageManager.shouldPreFetchData(repository, arbitraryTransactionData));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ import org.qortal.test.common.ArbitraryUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -55,6 +57,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
@@ -149,6 +152,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
@@ -181,6 +185,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
@@ -226,6 +231,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
@@ -294,6 +300,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
@@ -343,6 +350,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
@@ -380,6 +388,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
@@ -409,6 +418,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
@@ -435,6 +445,7 @@ public class ArbitraryDataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
|
||||
@@ -20,7 +20,9 @@ import org.qortal.test.common.ArbitraryUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -50,6 +52,7 @@ public class ArbitraryTransactionMetadataTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Create PUT transaction
|
||||
|
||||
@@ -19,7 +19,9 @@ import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.ArbitraryTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -46,6 +48,7 @@ public class ArbitraryTransactionTests extends Common {
|
||||
|
||||
// Register the name to Alice
|
||||
RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||
registerNameTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(registerNameTransactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, registerNameTransactionData, alice);
|
||||
|
||||
// Set difficulty to 1
|
||||
|
||||
@@ -20,7 +20,9 @@ import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.Amounts;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class BuySellTests extends Common {
|
||||
|
||||
@@ -62,6 +64,7 @@ public class BuySellTests extends Common {
|
||||
public void testRegisterName() throws DataException {
|
||||
// Register-name
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "{}");
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
String name = transactionData.getName();
|
||||
|
||||
@@ -11,7 +11,9 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,6 +35,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -56,6 +59,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "\uD83E\uDD73";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -82,6 +86,7 @@ public class IntegrityTests extends Common {
|
||||
String name = "initial_name";
|
||||
String data = "initial_data";
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Update the name, but keep the new name blank
|
||||
@@ -116,6 +121,7 @@ public class IntegrityTests extends Common {
|
||||
String name = "initial_name";
|
||||
String data = "initial_data";
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Update the name, but keep the new name blank
|
||||
@@ -143,6 +149,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -172,6 +179,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -210,6 +218,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -235,6 +244,7 @@ public class IntegrityTests extends Common {
|
||||
|
||||
// Attempt to register the new name
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), newName, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
@@ -254,6 +264,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -268,6 +279,7 @@ public class IntegrityTests extends Common {
|
||||
// Attempt to register the name again
|
||||
String duplicateName = "TEST-nÁme";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
@@ -287,6 +299,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -320,6 +333,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -329,6 +343,7 @@ public class IntegrityTests extends Common {
|
||||
String secondName = "new-missing-name";
|
||||
String secondNameData = "{\"data2\":true}";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), secondName, secondNameData);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the second name exists and the data is correct
|
||||
@@ -362,6 +377,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -393,6 +409,7 @@ public class IntegrityTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
|
||||
@@ -3,20 +3,26 @@ package org.qortal.test.naming;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.BlockMinter;
|
||||
import org.qortal.data.transaction.*;
|
||||
import org.qortal.naming.Name;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.test.common.*;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class MiscTests extends Common {
|
||||
|
||||
@@ -34,6 +40,7 @@ public class MiscTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
List<String> recentNames = repository.getNameRepository().getRecentNames(0L);
|
||||
@@ -53,11 +60,13 @@ public class MiscTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// duplicate
|
||||
String duplicateName = "TEST-nÁme";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), duplicateName, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
@@ -76,12 +85,14 @@ public class MiscTests extends Common {
|
||||
String data = "{}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// duplicate (this time registered by Bob)
|
||||
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||
String duplicateName = "TEST-nÁme";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(bob), duplicateName, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
@@ -100,12 +111,14 @@ public class MiscTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Register another name that we will later attempt to rename to first name (above)
|
||||
String otherName = "new-name";
|
||||
String otherData = "";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), otherName, otherData);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// we shouldn't be able to update name to existing name
|
||||
@@ -129,6 +142,7 @@ public class MiscTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
|
||||
@@ -147,6 +161,7 @@ public class MiscTests extends Common {
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// we shouldn't be able to update name to an address
|
||||
@@ -175,6 +190,7 @@ public class MiscTests extends Common {
|
||||
|
||||
// Register the name
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -201,6 +217,7 @@ public class MiscTests extends Common {
|
||||
|
||||
// Register the name
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -252,6 +269,7 @@ public class MiscTests extends Common {
|
||||
|
||||
// Register the name
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Ensure the name exists and the data is correct
|
||||
@@ -283,6 +301,7 @@ public class MiscTests extends Common {
|
||||
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
|
||||
// Ensure the name doesn't exist
|
||||
assertNull(repository.getNameRepository().fromName(name));
|
||||
@@ -304,4 +323,54 @@ public class MiscTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
// test name registration fee increase
|
||||
@Test
|
||||
public void testRegisterNameFeeIncrease() throws DataException, IllegalAccessException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Set nameRegistrationUnitFeeTimestamp to a time far in the future
|
||||
long futureTimestamp = 9999999999999L; // 20 Nov 2286
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFeeTimestamp", futureTimestamp, true);
|
||||
assertEquals(futureTimestamp, BlockChain.getInstance().getNameRegistrationUnitFeeTimestamp());
|
||||
|
||||
// Validate unit fees pre and post timestamp
|
||||
assertEquals(10000000, BlockChain.getInstance().getUnitFee()); // 0.1 QORT
|
||||
assertEquals(500000000, BlockChain.getInstance().getNameRegistrationUnitFee()); // 5 QORT
|
||||
|
||||
// Register-name
|
||||
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||
String name = "test-name";
|
||||
String data = "{\"age\":30}";
|
||||
|
||||
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
assertEquals(10000000L, transactionData.getFee().longValue());
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Set nameRegistrationUnitFeeTimestamp to a time in the past
|
||||
Long now = NTP.getTime();
|
||||
FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFeeTimestamp", now - 1000L, true);
|
||||
assertEquals(now - 1000L, BlockChain.getInstance().getNameRegistrationUnitFeeTimestamp());
|
||||
|
||||
// Register a different name
|
||||
// First try with the default unit fee
|
||||
String name2 = "test-name-2";
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name2, data);
|
||||
assertEquals(10000000L, transactionData.getFee().longValue());
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
ValidationResult result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be invalid", ValidationResult.INSUFFICIENT_FEE == result);
|
||||
|
||||
// Now try using correct fee (this is specified by the UI, via the /transaction/unitfee API endpoint)
|
||||
transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name2, data);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
assertEquals(500000000L, transactionData.getFee().longValue());
|
||||
transaction = Transaction.fromData(repository, transactionData);
|
||||
transaction.sign(alice);
|
||||
result = transaction.importAsUnconfirmed();
|
||||
assertTrue("Transaction should be valid", ValidationResult.OK == result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.test.common.transaction.TestTransaction;
|
||||
import org.qortal.transaction.RegisterNameTransaction;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class UpdateTests extends Common {
|
||||
|
||||
@@ -34,6 +36,7 @@ public class UpdateTests extends Common {
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
initialTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(initialTransactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||
|
||||
// Check name, reduced name, and data exist
|
||||
@@ -100,6 +103,7 @@ public class UpdateTests extends Common {
|
||||
String constantReducedName = "initia1-name";
|
||||
|
||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
initialTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(initialTransactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
@@ -147,6 +151,7 @@ public class UpdateTests extends Common {
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData initialTransactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
initialTransactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(initialTransactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, initialTransactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
@@ -225,6 +230,7 @@ public class UpdateTests extends Common {
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
@@ -282,6 +288,7 @@ public class UpdateTests extends Common {
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
@@ -323,6 +330,7 @@ public class UpdateTests extends Common {
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
@@ -385,6 +393,7 @@ public class UpdateTests extends Common {
|
||||
String initialData = "{\"age\":30}";
|
||||
|
||||
TransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), initialName, initialData);
|
||||
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));;
|
||||
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||
|
||||
// Check initial name exists
|
||||
|
||||
114
src/test/java/org/qortal/test/network/OnlineAccountsTests.java
Normal file
114
src/test/java/org/qortal/test/network/OnlineAccountsTests.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package org.qortal.test.network;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||
import org.junit.Test;
|
||||
import org.qortal.data.network.OnlineAccountData;
|
||||
import org.qortal.network.message.*;
|
||||
import org.qortal.transform.Transformer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.Security;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class OnlineAccountsTests {
|
||||
|
||||
private static final Random RANDOM = new Random();
|
||||
static {
|
||||
// This must go before any calls to LogManager/Logger
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGetOnlineAccountsV2() throws Message.MessageException {
|
||||
List<OnlineAccountData> onlineAccountsOut = generateOnlineAccounts(false);
|
||||
|
||||
Message messageOut = new GetOnlineAccountsV2Message(onlineAccountsOut);
|
||||
|
||||
byte[] messageBytes = messageOut.toBytes();
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(messageBytes);
|
||||
|
||||
GetOnlineAccountsV2Message messageIn = (GetOnlineAccountsV2Message) Message.fromByteBuffer(byteBuffer);
|
||||
|
||||
List<OnlineAccountData> onlineAccountsIn = messageIn.getOnlineAccounts();
|
||||
|
||||
assertEquals("size mismatch", onlineAccountsOut.size(), onlineAccountsIn.size());
|
||||
assertTrue("accounts mismatch", onlineAccountsIn.containsAll(onlineAccountsOut));
|
||||
|
||||
Message oldMessageOut = new GetOnlineAccountsMessage(onlineAccountsOut);
|
||||
byte[] oldMessageBytes = oldMessageOut.toBytes();
|
||||
|
||||
long numTimestamps = onlineAccountsOut.stream().mapToLong(OnlineAccountData::getTimestamp).sorted().distinct().count();
|
||||
|
||||
System.out.println(String.format("For %d accounts split across %d timestamp%s: old size %d vs new size %d",
|
||||
onlineAccountsOut.size(),
|
||||
numTimestamps,
|
||||
numTimestamps != 1 ? "s" : "",
|
||||
oldMessageBytes.length,
|
||||
messageBytes.length));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnlineAccountsV2() throws Message.MessageException {
|
||||
List<OnlineAccountData> onlineAccountsOut = generateOnlineAccounts(true);
|
||||
|
||||
Message messageOut = new OnlineAccountsV2Message(onlineAccountsOut);
|
||||
|
||||
byte[] messageBytes = messageOut.toBytes();
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(messageBytes);
|
||||
|
||||
OnlineAccountsV2Message messageIn = (OnlineAccountsV2Message) Message.fromByteBuffer(byteBuffer);
|
||||
|
||||
List<OnlineAccountData> onlineAccountsIn = messageIn.getOnlineAccounts();
|
||||
|
||||
assertEquals("size mismatch", onlineAccountsOut.size(), onlineAccountsIn.size());
|
||||
assertTrue("accounts mismatch", onlineAccountsIn.containsAll(onlineAccountsOut));
|
||||
|
||||
Message oldMessageOut = new OnlineAccountsMessage(onlineAccountsOut);
|
||||
byte[] oldMessageBytes = oldMessageOut.toBytes();
|
||||
|
||||
long numTimestamps = onlineAccountsOut.stream().mapToLong(OnlineAccountData::getTimestamp).sorted().distinct().count();
|
||||
|
||||
System.out.println(String.format("For %d accounts split across %d timestamp%s: old size %d vs new size %d",
|
||||
onlineAccountsOut.size(),
|
||||
numTimestamps,
|
||||
numTimestamps != 1 ? "s" : "",
|
||||
oldMessageBytes.length,
|
||||
messageBytes.length));
|
||||
}
|
||||
|
||||
private List<OnlineAccountData> generateOnlineAccounts(boolean withSignatures) {
|
||||
List<OnlineAccountData> onlineAccounts = new ArrayList<>();
|
||||
|
||||
int numTimestamps = RANDOM.nextInt(2) + 1; // 1 or 2
|
||||
|
||||
for (int t = 0; t < numTimestamps; ++t) {
|
||||
int numAccounts = RANDOM.nextInt(3000);
|
||||
|
||||
for (int a = 0; a < numAccounts; ++a) {
|
||||
byte[] sig = null;
|
||||
if (withSignatures) {
|
||||
sig = new byte[Transformer.SIGNATURE_LENGTH];
|
||||
RANDOM.nextBytes(sig);
|
||||
}
|
||||
|
||||
byte[] pubkey = new byte[Transformer.PUBLIC_KEY_LENGTH];
|
||||
RANDOM.nextBytes(pubkey);
|
||||
|
||||
onlineAccounts.add(new OnlineAccountData(t << 32, sig, pubkey));
|
||||
}
|
||||
}
|
||||
|
||||
return onlineAccounts;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"maxBlockSize": 2097152,
|
||||
"maxBytesPerUnitFee": 1024,
|
||||
"unitFee": "0.1",
|
||||
"nameRegistrationUnitFee": "5",
|
||||
"requireGroupForApproval": false,
|
||||
"minAccountLevelToRewardShare": 5,
|
||||
"maxRewardSharesPerMintingAccount": 20,
|
||||
|
||||
Reference in New Issue
Block a user