diff --git a/TestNets.md b/TestNets.md
index e475e593..b4b9feed 100644
--- a/TestNets.md
+++ b/TestNets.md
@@ -52,14 +52,13 @@
## Single-node testnet
-A single-node testnet is possible with code modifications, for basic testing, or to more easily start a new testnet.
-To do so, follow these steps:
-- Comment out the `if (mintedLastBlock) { }` conditional in BlockMinter.java
-- Comment out the `minBlockchainPeers` validation in Settings.validate()
-- Set `minBlockchainPeers` to 0 in settings.json
-- Set `Synchronizer.RECOVERY_MODE_TIMEOUT` to `0`
-- All other steps should remain the same. Only a single reward share key is needed.
-- Remember to put these values back after introducing other nodes
+A single-node testnet is possible with an additional settings, or to more easily start a new testnet.
+Just add this setting:
+```
+"singleNodeTestnet": true
+```
+This will automatically allow multiple consecutive blocks to be minted, as well as setting minBlockchainPeers to 0.
+Remember to put these values back after introducing other nodes
## Fixed network
@@ -93,3 +92,32 @@ Your options are:
- `qort` tool, but prepend with one-time shell variable: `BASE_URL=some-node-hostname-or-ip:port qort ......`
- `peer-heights`, but use `-t` option, or `BASE_URL` shell variable as above
+## Example settings-test.json
+```
+{
+ "isTestNet": true,
+ "bitcoinNet": "TEST3",
+ "repositoryPath": "db-testnet",
+ "blockchainConfig": "testchain.json",
+ "minBlockchainPeers": 1,
+ "apiDocumentationEnabled": true,
+ "apiRestricted": false,
+ "bootstrap": false,
+ "maxPeerConnectionTime": 999999999,
+ "localAuthBypassEnabled": true,
+ "singleNodeTestnet": true,
+ "recoveryModeTimeout": 0
+}
+```
+
+## Quick start
+Here are some steps to quickly get a single node testnet up and running with a generic minting account:
+1. Start with template `settings-test.json`, and create a `testchain.json` based on mainnet's blockchain.json (or obtain one from Qortal developers). These should be in the same directory as the jar.
+2. Make sure feature triggers and other timestamp/height activations are correctly set. Generally these would be `0` so that they are enabled from the start.
+3. Set a recent genesis `timestamp` in testchain.json, and add this reward share entry:
+`{ "type": "REWARD_SHARE", "minterPublicKey": "DwcUnhxjamqppgfXCLgbYRx8H9XFPUc2qYRy3CEvQWEw", "recipient": "QbTDMss7NtRxxQaSqBZtSLSNdSYgvGaqFf", "rewardSharePublicKey": "CRvQXxFfUMfr4q3o1PcUZPA4aPCiubBsXkk47GzRo754", "sharePercent": 0 },`
+4. Start the node, passing in settings-test.json, e.g: `java -jar qortal.jar settings-test.json`
+5. Once started, add the corresponding minting key to the node:
+`curl -X POST "http://localhost:62391/admin/mintingaccounts" -d "F48mYJycFgRdqtc58kiovwbcJgVukjzRE4qRRtRsK9ix"`
+6. Alternatively you can use your own minting account instead of the generic one above.
+7. After a short while, blocks should be minted from the genesis timestamp until the current time.
\ No newline at end of file
diff --git a/WindowsInstaller/Qortal.aip b/WindowsInstaller/Qortal.aip
index 74acc012..1f579a9c 100755
--- a/WindowsInstaller/Qortal.aip
+++ b/WindowsInstaller/Qortal.aip
@@ -17,10 +17,10 @@
-
+
-
+
@@ -212,7 +212,7 @@
-
+
@@ -1173,7 +1173,7 @@
-
+
diff --git a/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
new file mode 100644
index 00000000..c2c3d355
Binary files /dev/null and b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar differ
diff --git a/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
new file mode 100644
index 00000000..0dc1aedc
--- /dev/null
+++ b/lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
@@ -0,0 +1,9 @@
+
+
+ 4.0.0
+ org.ciyam
+ AT
+ 1.4.0
+ POM was created from install:install-file
+
diff --git a/lib/org/ciyam/AT/maven-metadata-local.xml b/lib/org/ciyam/AT/maven-metadata-local.xml
index 8f8b1f6e..063c735d 100644
--- a/lib/org/ciyam/AT/maven-metadata-local.xml
+++ b/lib/org/ciyam/AT/maven-metadata-local.xml
@@ -3,14 +3,15 @@
org.ciyam
AT
- 1.3.8
+ 1.4.0
1.3.4
1.3.5
1.3.6
1.3.7
1.3.8
+ 1.4.0
- 20200925114415
+ 20221105114346
diff --git a/pom.xml b/pom.xml
index 22017136..860cdce5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
org.qortal
qortal
- 3.5.0
+ 3.6.4
jar
true
@@ -11,7 +11,7 @@
0.15.10
1.69
${maven.build.timestamp}
- 1.3.8
+ 1.4.0
3.6
1.8
2.6
diff --git a/src/main/java/org/qortal/api/model/ConnectedPeer.java b/src/main/java/org/qortal/api/model/ConnectedPeer.java
index 21bfc1f9..3d383321 100644
--- a/src/main/java/org/qortal/api/model/ConnectedPeer.java
+++ b/src/main/java/org/qortal/api/model/ConnectedPeer.java
@@ -1,7 +1,7 @@
package org.qortal.api.model;
import io.swagger.v3.oas.annotations.media.Schema;
-import org.qortal.data.network.PeerChainTipData;
+import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.network.Handshake;
import org.qortal.network.Peer;
@@ -63,11 +63,11 @@ public class ConnectedPeer {
this.age = "connecting...";
}
- PeerChainTipData peerChainTipData = peer.getChainTipData();
+ BlockSummaryData peerChainTipData = peer.getChainTipData();
if (peerChainTipData != null) {
- this.lastHeight = peerChainTipData.getLastHeight();
- this.lastBlockSignature = peerChainTipData.getLastBlockSignature();
- this.lastBlockTimestamp = peerChainTipData.getLastBlockTimestamp();
+ this.lastHeight = peerChainTipData.getHeight();
+ this.lastBlockSignature = peerChainTipData.getSignature();
+ this.lastBlockTimestamp = peerChainTipData.getTimestamp();
}
}
diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java
index 4de8d908..468b90a8 100644
--- a/src/main/java/org/qortal/api/resource/AddressesResource.java
+++ b/src/main/java/org/qortal/api/resource/AddressesResource.java
@@ -205,6 +205,10 @@ public class AddressesResource {
try (final Repository repository = RepositoryManager.getRepository()) {
List onlineAccountLevels = new ArrayList<>();
+ // Prepopulate all levels
+ for (int i=0; i<=10; i++)
+ onlineAccountLevels.add(new OnlineAccountLevel(i, 0));
+
for (OnlineAccountData onlineAccountData : onlineAccounts) {
try {
final int minterLevel = Account.getRewardShareEffectiveMintingLevelIncludingLevelZero(repository, onlineAccountData.getPublicKey());
diff --git a/src/main/java/org/qortal/api/resource/ChatResource.java b/src/main/java/org/qortal/api/resource/ChatResource.java
index 0bbd1951..ee2a8599 100644
--- a/src/main/java/org/qortal/api/resource/ChatResource.java
+++ b/src/main/java/org/qortal/api/resource/ChatResource.java
@@ -69,6 +69,7 @@ public class ChatResource {
public List searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
@QueryParam("txGroupId") Integer txGroupId,
@QueryParam("involving") List involvingAddresses,
+ @QueryParam("reference") String reference,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
@@ -87,11 +88,16 @@ public class ChatResource {
if (after != null && after < 1500000000000L)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
+ byte[] referenceBytes = null;
+ if (reference != null)
+ referenceBytes = Base58.decode(reference);
+
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getMessagesMatchingCriteria(
before,
after,
txGroupId,
+ referenceBytes,
involvingAddresses,
limit, offset, reverse);
} catch (DataException e) {
diff --git a/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java b/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java
index 3dc2d494..9760b7f0 100644
--- a/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java
+++ b/src/main/java/org/qortal/api/websocket/ChatMessagesWebSocket.java
@@ -46,6 +46,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
null,
txGroupId,
null,
+ null,
null, null, null);
sendMessages(session, chatMessages);
@@ -72,6 +73,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
null,
null,
null,
+ null,
involvingAddresses,
null, null, null);
diff --git a/src/main/java/org/qortal/arbitrary/misc/Service.java b/src/main/java/org/qortal/arbitrary/misc/Service.java
index 5d94d806..5dd8d94e 100644
--- a/src/main/java/org/qortal/arbitrary/misc/Service.java
+++ b/src/main/java/org/qortal/arbitrary/misc/Service.java
@@ -1,16 +1,18 @@
package org.qortal.arbitrary.misc;
+import org.apache.commons.io.FilenameUtils;
import org.json.JSONObject;
import org.qortal.arbitrary.ArbitraryDataRenderer;
import org.qortal.transaction.Transaction;
import org.qortal.utils.FilesystemUtils;
+import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
@@ -38,6 +40,7 @@ public enum Service {
GIT_REPOSITORY(300, false, null, null),
IMAGE(400, true, 10*1024*1024L, null),
THUMBNAIL(410, true, 500*1024L, null),
+ QCHAT_IMAGE(420, true, 500*1024L, null),
VIDEO(500, false, null, null),
AUDIO(600, false, null, null),
BLOG(700, false, null, null),
@@ -48,7 +51,30 @@ public enum Service {
PLAYLIST(910, true, null, null),
APP(1000, false, null, null),
METADATA(1100, false, null, null),
- QORTAL_METADATA(1111, true, 10*1024L, Arrays.asList("title", "description", "tags"));
+ GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
+ @Override
+ public ValidationResult validate(Path path) {
+ // Custom validation function to require .gif files only, and at least 1
+ int gifCount = 0;
+ File[] files = path.toFile().listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ return ValidationResult.DIRECTORIES_NOT_ALLOWED;
+ }
+ String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
+ if (!Objects.equals(extension, "gif")) {
+ return ValidationResult.INVALID_FILE_EXTENSION;
+ }
+ gifCount++;
+ }
+ }
+ if (gifCount == 0) {
+ return ValidationResult.MISSING_DATA;
+ }
+ return ValidationResult.OK;
+ }
+ };
public final int value;
private final boolean requiresValidation;
@@ -114,7 +140,10 @@ public enum Service {
OK(1),
MISSING_KEYS(2),
EXCEEDS_SIZE_LIMIT(3),
- MISSING_INDEX_FILE(4);
+ MISSING_INDEX_FILE(4),
+ DIRECTORIES_NOT_ALLOWED(5),
+ INVALID_FILE_EXTENSION(6),
+ MISSING_DATA(7);
public final int value;
diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java
index e0581e7d..5e838458 100644
--- a/src/main/java/org/qortal/block/Block.java
+++ b/src/main/java/org/qortal/block/Block.java
@@ -366,18 +366,14 @@ public class Block {
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
- // Fetch our list of online accounts
+ // Fetch our list of online accounts, removing any that are missing a nonce
List onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
+ onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
if (onlineAccounts.isEmpty()) {
- LOGGER.error("No online accounts - not even our own?");
+ LOGGER.debug("No online accounts - not even our own?");
return null;
}
- // If mempow is active, remove any legacy accounts that are missing a nonce
- if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
- }
-
// Load sorted list of reward share public keys into memory, so that the indexes can be obtained.
// This is up to 100x faster than querying each index separately. For 4150 reward share keys, it
// was taking around 5000ms to query individually, vs 50ms using this approach.
@@ -411,29 +407,27 @@ public class Block {
// Aggregated, single signature
byte[] onlineAccountsSignatures = Qortal25519Extras.aggregateSignatures(signaturesToAggregate);
- // Add nonces to the end of the online accounts signatures if mempow is active
- if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- try {
- // Create ordered list of nonce values
- List nonces = new ArrayList<>();
- for (int i = 0; i < onlineAccountsCount; ++i) {
- Integer accountIndex = accountIndexes.get(i);
- OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
- nonces.add(onlineAccountData.getNonce());
- }
-
- // Encode the nonces to a byte array
- byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
-
- // Append the encoded nonces to the encoded online account signatures
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- outputStream.write(onlineAccountsSignatures);
- outputStream.write(encodedNonces);
- onlineAccountsSignatures = outputStream.toByteArray();
- }
- catch (TransformationException | IOException e) {
- return null;
+ // Add nonces to the end of the online accounts signatures
+ try {
+ // Create ordered list of nonce values
+ List nonces = new ArrayList<>();
+ for (int i = 0; i < onlineAccountsCount; ++i) {
+ Integer accountIndex = accountIndexes.get(i);
+ OnlineAccountData onlineAccountData = indexedOnlineAccounts.get(accountIndex);
+ nonces.add(onlineAccountData.getNonce());
}
+
+ // Encode the nonces to a byte array
+ byte[] encodedNonces = BlockTransformer.encodeOnlineAccountNonces(nonces);
+
+ // Append the encoded nonces to the encoded online account signatures
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ outputStream.write(onlineAccountsSignatures);
+ outputStream.write(encodedNonces);
+ onlineAccountsSignatures = outputStream.toByteArray();
+ }
+ catch (TransformationException | IOException e) {
+ return null;
}
byte[] minterSignature = minter.sign(BlockTransformer.getBytesForMinterSignature(parentBlockData,
@@ -1046,14 +1040,9 @@ public class Block {
final int signaturesLength = Transformer.SIGNATURE_LENGTH;
final int noncesLength = onlineRewardShares.size() * Transformer.INT_LENGTH;
- if (this.blockData.getTimestamp() >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- // We expect nonces to be appended to the online accounts signatures
- if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength + noncesLength)
- return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
- } else {
- if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength)
- return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
- }
+ // We expect nonces to be appended to the online accounts signatures
+ if (this.blockData.getOnlineAccountsSignatures().length != signaturesLength + noncesLength)
+ return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
// Check signatures
long onlineTimestamp = this.blockData.getOnlineAccountsTimestamp();
@@ -1062,32 +1051,33 @@ public class Block {
byte[] encodedOnlineAccountSignatures = this.blockData.getOnlineAccountsSignatures();
// Split online account signatures into signature(s) + nonces, then validate the nonces
- if (this.blockData.getTimestamp() >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength);
- byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH);
- encodedOnlineAccountSignatures = extractedSignatures;
+ byte[] extractedSignatures = BlockTransformer.extract(encodedOnlineAccountSignatures, 0, signaturesLength);
+ byte[] extractedNonces = BlockTransformer.extract(encodedOnlineAccountSignatures, signaturesLength, onlineRewardShares.size() * Transformer.INT_LENGTH);
+ encodedOnlineAccountSignatures = extractedSignatures;
- List nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
+ List nonces = BlockTransformer.decodeOnlineAccountNonces(extractedNonces);
- // Build block's view of online accounts (without signatures, as we don't need them here)
- Set onlineAccounts = new HashSet<>();
- for (int i = 0; i < onlineRewardShares.size(); ++i) {
- Integer nonce = nonces.get(i);
- byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
+ // Build block's view of online accounts (without signatures, as we don't need them here)
+ Set onlineAccounts = new HashSet<>();
+ for (int i = 0; i < onlineRewardShares.size(); ++i) {
+ Integer nonce = nonces.get(i);
+ byte[] publicKey = onlineRewardShares.get(i).getRewardSharePublicKey();
- OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, null, publicKey, nonce);
- onlineAccounts.add(onlineAccountData);
- }
-
- // Remove those already validated & cached by online accounts manager - no need to re-validate them
- OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
-
- // Validate the rest
- for (OnlineAccountData onlineAccount : onlineAccounts)
- if (!OnlineAccountsManager.getInstance().verifyMemoryPoW(onlineAccount, this.blockData.getTimestamp()))
- return ValidationResult.ONLINE_ACCOUNT_NONCE_INCORRECT;
+ OnlineAccountData onlineAccountData = new OnlineAccountData(onlineTimestamp, null, publicKey, nonce);
+ onlineAccounts.add(onlineAccountData);
}
+ // Remove those already validated & cached by online accounts manager - no need to re-validate them
+ OnlineAccountsManager.getInstance().removeKnown(onlineAccounts, onlineTimestamp);
+
+ // Validate the rest
+ for (OnlineAccountData onlineAccount : onlineAccounts)
+ if (!OnlineAccountsManager.getInstance().verifyMemoryPoW(onlineAccount, null))
+ return ValidationResult.ONLINE_ACCOUNT_NONCE_INCORRECT;
+
+ // Cache the valid online accounts as they will likely be needed for the next block
+ OnlineAccountsManager.getInstance().addBlocksOnlineAccounts(onlineAccounts, onlineTimestamp);
+
// Extract online accounts' timestamp signatures from block data. Only one signature if aggregated.
List onlineAccountsSignatures = BlockTransformer.decodeTimestampSignatures(encodedOnlineAccountSignatures);
diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java
index 42692a18..5e1f44f3 100644
--- a/src/main/java/org/qortal/block/BlockChain.java
+++ b/src/main/java/org/qortal/block/BlockChain.java
@@ -73,7 +73,8 @@ public class BlockChain {
calcChainWeightTimestamp,
transactionV5Timestamp,
transactionV6Timestamp,
- disableReferenceTimestamp;
+ disableReferenceTimestamp,
+ increaseOnlineAccountsDifficultyTimestamp;
}
// Custom transaction fees
@@ -195,10 +196,6 @@ public class BlockChain {
* featureTriggers because unit tests need to set this value via Reflection. */
private long onlineAccountsModulusV2Timestamp;
- /** Feature trigger timestamp for online accounts mempow verification. Can't use featureTriggers
- * because unit tests need to set this value via Reflection. */
- private long onlineAccountsMemoryPoWTimestamp;
-
/** Max reward shares by block height */
public static class MaxRewardSharesByTimestamp {
public long timestamp;
@@ -359,10 +356,6 @@ public class BlockChain {
return this.onlineAccountsModulusV2Timestamp;
}
- public long getOnlineAccountsMemoryPoWTimestamp() {
- return this.onlineAccountsMemoryPoWTimestamp;
- }
-
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
public boolean getRequireGroupForApproval() {
return this.requireGroupForApproval;
@@ -486,6 +479,10 @@ public class BlockChain {
return this.featureTriggers.get(FeatureTrigger.disableReferenceTimestamp.name()).longValue();
}
+ public long getIncreaseOnlineAccountsDifficultyTimestamp() {
+ return this.featureTriggers.get(FeatureTrigger.increaseOnlineAccountsDifficultyTimestamp.name()).longValue();
+ }
+
// More complex getters for aspects that change by height or timestamp
diff --git a/src/main/java/org/qortal/controller/BlockMinter.java b/src/main/java/org/qortal/controller/BlockMinter.java
index 343ab4af..7e3b4b9e 100644
--- a/src/main/java/org/qortal/controller/BlockMinter.java
+++ b/src/main/java/org/qortal/controller/BlockMinter.java
@@ -26,6 +26,9 @@ import org.qortal.data.block.CommonBlockData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.Network;
import org.qortal.network.Peer;
+import org.qortal.network.message.BlockSummariesV2Message;
+import org.qortal.network.message.HeightV2Message;
+import org.qortal.network.message.Message;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -90,6 +93,8 @@ public class BlockMinter extends Thread {
List newBlocks = new ArrayList<>();
+ final boolean isSingleNodeTestnet = Settings.getInstance().isSingleNodeTestnet();
+
try (final Repository repository = RepositoryManager.getRepository()) {
// Going to need this a lot...
BlockRepository blockRepository = repository.getBlockRepository();
@@ -108,8 +113,9 @@ public class BlockMinter extends Thread {
// Free up any repository locks
repository.discardChanges();
- // Sleep for a while
- Thread.sleep(1000);
+ // Sleep for a while.
+ // It's faster on single node testnets, to allow lots of blocks to be minted quickly.
+ Thread.sleep(isSingleNodeTestnet ? 50 : 1000);
isMintingPossible = false;
@@ -220,9 +226,10 @@ public class BlockMinter extends Thread {
List newBlocksMintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
// We might need to sit the next block out, if one of our minting accounts signed the previous one
+ // Skip this check for single node testnets, since they definitely need to mint every block
byte[] previousBlockMinter = previousBlockData.getMinterPublicKey();
boolean mintedLastBlock = mintingAccountsData.stream().anyMatch(mintingAccount -> Arrays.equals(mintingAccount.getPublicKey(), previousBlockMinter));
- if (mintedLastBlock) {
+ if (mintedLastBlock && !isSingleNodeTestnet) {
LOGGER.trace(String.format("One of our keys signed the last block, so we won't sign the next one"));
continue;
}
@@ -241,7 +248,7 @@ public class BlockMinter extends Thread {
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
if (newBlock == null) {
// For some reason we can't mint right now
- moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
+ moderatedLog(() -> LOGGER.info("Couldn't build a to-be-minted block"));
continue;
}
@@ -433,11 +440,9 @@ public class BlockMinter extends Thread {
if (newBlockMinted) {
// Broadcast our new chain to network
- BlockData newBlockData = newBlock.getBlockData();
-
- Network network = Network.getInstance();
- network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
+ Network.getInstance().broadcastOurChain();
}
+
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index 4ff08e15..bcd010e8 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -45,7 +45,6 @@ import org.qortal.data.account.AccountData;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.naming.NameData;
-import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
@@ -317,6 +316,10 @@ public class Controller extends Thread {
}
}
+ public static long uptime() {
+ return System.currentTimeMillis() - Controller.startTime;
+ }
+
/** Returns highest block, or null if it's not available. */
public BlockData getChainTip() {
synchronized (this.latestBlocks) {
@@ -727,25 +730,25 @@ public class Controller extends Thread {
public static final Predicate hasNoRecentBlock = peer -> {
final Long minLatestBlockTimestamp = getMinimumLatestBlockTimestamp();
- final PeerChainTipData peerChainTipData = peer.getChainTipData();
- return peerChainTipData == null || peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp;
+ final BlockSummaryData peerChainTipData = peer.getChainTipData();
+ return peerChainTipData == null || peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp;
};
public static final Predicate hasNoOrSameBlock = peer -> {
final BlockData latestBlockData = getInstance().getChainTip();
- final PeerChainTipData peerChainTipData = peer.getChainTipData();
- return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getLastBlockSignature());
+ final BlockSummaryData peerChainTipData = peer.getChainTipData();
+ return peerChainTipData == null || peerChainTipData.getSignature() == null || Arrays.equals(latestBlockData.getSignature(), peerChainTipData.getSignature());
};
public static final Predicate hasOnlyGenesisBlock = peer -> {
- final PeerChainTipData peerChainTipData = peer.getChainTipData();
- return peerChainTipData == null || peerChainTipData.getLastHeight() == null || peerChainTipData.getLastHeight() == 1;
+ final BlockSummaryData peerChainTipData = peer.getChainTipData();
+ return peerChainTipData == null || peerChainTipData.getHeight() == 1;
};
public static final Predicate hasInferiorChainTip = peer -> {
- final PeerChainTipData peerChainTipData = peer.getChainTipData();
+ final BlockSummaryData peerChainTipData = peer.getChainTipData();
final List inferiorChainTips = Synchronizer.getInstance().inferiorChainSignatures;
- return peerChainTipData == null || peerChainTipData.getLastBlockSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getLastBlockSignature()));
+ return peerChainTipData == null || peerChainTipData.getSignature() == null || inferiorChainTips.contains(ByteArray.wrap(peerChainTipData.getSignature()));
};
public static final Predicate hasOldVersion = peer -> {
@@ -835,6 +838,12 @@ public class Controller extends Thread {
String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText);
if (!Settings.getInstance().isLite()) {
tooltip = tooltip.concat(String.format(" - %s %d", heightText, height));
+
+ final Integer blocksRemaining = Synchronizer.getInstance().getBlocksRemaining();
+ if (blocksRemaining != null && blocksRemaining > 0) {
+ String blocksRemainingText = Translator.INSTANCE.translate("SysTray", "BLOCKS_REMAINING");
+ tooltip = tooltip.concat(String.format(" - %d %s", blocksRemaining, blocksRemainingText));
+ }
}
tooltip = tooltip.concat(String.format("\n%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion));
SysTray.getInstance().setToolTipText(tooltip);
@@ -1007,8 +1016,7 @@ public class Controller extends Thread {
network.broadcast(peer -> peer.isOutbound() ? network.buildPeersMessage(peer) : new GetPeersMessage());
// Send our current height
- BlockData latestBlockData = getChainTip();
- network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
+ network.broadcastOurChain();
// Request unconfirmed transaction signatures, but only if we're up-to-date.
// If we're NOT up-to-date then priority is synchronizing first
@@ -1215,6 +1223,10 @@ public class Controller extends Thread {
onNetworkHeightV2Message(peer, message);
break;
+ case BLOCK_SUMMARIES_V2:
+ onNetworkBlockSummariesV2Message(peer, message);
+ break;
+
case GET_TRANSACTION:
TransactionImporter.getInstance().onNetworkGetTransactionMessage(peer, message);
break;
@@ -1232,19 +1244,10 @@ public class Controller extends Thread {
break;
case GET_ONLINE_ACCOUNTS:
- OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsMessage(peer, message);
- break;
-
case ONLINE_ACCOUNTS:
- OnlineAccountsManager.getInstance().onNetworkOnlineAccountsMessage(peer, message);
- break;
-
case GET_ONLINE_ACCOUNTS_V2:
- OnlineAccountsManager.getInstance().onNetworkGetOnlineAccountsV2Message(peer, message);
- break;
-
case ONLINE_ACCOUNTS_V2:
- OnlineAccountsManager.getInstance().onNetworkOnlineAccountsV2Message(peer, message);
+ // No longer supported - to be eventually removed
break;
case GET_ONLINE_ACCOUNTS_V3:
@@ -1378,8 +1381,10 @@ public class Controller extends Thread {
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'block unknown' response to peer %s for GET_BLOCK request for unknown block %s", peer, Base58.encode(signature)));
- // We'll send empty block summaries message as it's very short
- Message blockUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+ // Send generic 'unknown' message as it's very short
+ Message blockUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
+ ? new GenericUnknownMessage()
+ : new BlockSummariesMessage(Collections.emptyList());
blockUnknownMessage.setId(message.getId());
if (!peer.sendMessage(blockUnknownMessage))
peer.disconnect("failed to send block-unknown response");
@@ -1428,11 +1433,15 @@ public class Controller extends Thread {
this.stats.getBlockSummariesStats.requests.incrementAndGet();
// If peer's parent signature matches our latest block signature
- // then we can short-circuit with an empty response
+ // then we have no blocks after that and can short-circuit with an empty response
BlockData chainTip = getChainTip();
if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) {
- Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList());
+ Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
+ ? new BlockSummariesV2Message(Collections.emptyList())
+ : new BlockSummariesMessage(Collections.emptyList());
+
blockSummariesMessage.setId(message.getId());
+
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
@@ -1488,7 +1497,9 @@ public class Controller extends Thread {
this.stats.getBlockSummariesStats.fullyFromCache.incrementAndGet();
}
- Message blockSummariesMessage = new BlockSummariesMessage(blockSummaries);
+ Message blockSummariesMessage = peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
+ ? new BlockSummariesV2Message(blockSummaries)
+ : new BlockSummariesMessage(blockSummaries);
blockSummariesMessage.setId(message.getId());
if (!peer.sendMessage(blockSummariesMessage))
peer.disconnect("failed to send block summaries");
@@ -1563,18 +1574,59 @@ public class Controller extends Thread {
// If peer is inbound and we've not updated their height
// then this is probably their initial HEIGHT_V2 message
// so they need a corresponding HEIGHT_V2 message from us
- if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
- peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
+ if (!peer.isOutbound() && peer.getChainTipData() == null) {
+ Message responseMessage = Network.getInstance().buildHeightOrChainTipInfo(peer);
+
+ if (responseMessage == null || !peer.sendMessage(responseMessage)) {
+ peer.disconnect("failed to send our chain tip info");
+ return;
+ }
+ }
}
// Update peer chain tip data
- PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
+ BlockSummaryData newChainTipData = new BlockSummaryData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getMinterPublicKey(), heightV2Message.getTimestamp());
peer.setChainTipData(newChainTipData);
// Potentially synchronize
Synchronizer.getInstance().requestSync();
}
+ private void onNetworkBlockSummariesV2Message(Peer peer, Message message) {
+ BlockSummariesV2Message blockSummariesV2Message = (BlockSummariesV2Message) message;
+
+ if (!Settings.getInstance().isLite()) {
+ // If peer is inbound and we've not updated their height
+ // then this is probably their initial BLOCK_SUMMARIES_V2 message
+ // so they need a corresponding BLOCK_SUMMARIES_V2 message from us
+ if (!peer.isOutbound() && peer.getChainTipData() == null) {
+ Message responseMessage = Network.getInstance().buildHeightOrChainTipInfo(peer);
+
+ if (responseMessage == null || !peer.sendMessage(responseMessage)) {
+ peer.disconnect("failed to send our chain tip info");
+ return;
+ }
+ }
+ }
+
+ if (message.hasId()) {
+ /*
+ * Experimental proof-of-concept: discard messages with ID
+ * These are 'late' reply messages received after timeout has expired,
+ * having been passed upwards from Peer to Network to Controller.
+ * Hence, these are NOT simple "here's my chain tip" broadcasts from other peers.
+ */
+ LOGGER.debug("Discarding late {} message with ID {} from {}", message.getType().name(), message.getId(), peer);
+ return;
+ }
+
+ // Update peer chain tip data
+ peer.setChainTipSummaries(blockSummariesV2Message.getBlockSummaries());
+
+ // Potentially synchronize
+ Synchronizer.getInstance().requestSync();
+ }
+
private void onNetworkGetAccountMessage(Peer peer, Message message) {
GetAccountMessage getAccountMessage = (GetAccountMessage) message;
String address = getAccountMessage.getAddress();
@@ -1590,8 +1642,8 @@ public class Controller extends Thread {
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s", peer, address));
- // We'll send empty block summaries message as it's very short
- Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+ // Send generic 'unknown' message as it's very short
+ Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
if (!peer.sendMessage(accountUnknownMessage))
peer.disconnect("failed to send account-unknown response");
@@ -1626,8 +1678,8 @@ public class Controller extends Thread {
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_BALANCE request for unknown account %s and asset ID %d", peer, address, assetId));
- // We'll send empty block summaries message as it's very short
- Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+ // Send generic 'unknown' message as it's very short
+ Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
if (!peer.sendMessage(accountUnknownMessage))
peer.disconnect("failed to send account-unknown response");
@@ -1670,8 +1722,8 @@ public class Controller extends Thread {
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_TRANSACTIONS request for unknown account %s", peer, address));
- // We'll send empty block summaries message as it's very short
- Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+ // Send generic 'unknown' message as it's very short
+ Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
if (!peer.sendMessage(accountUnknownMessage))
peer.disconnect("failed to send account-unknown response");
@@ -1707,8 +1759,8 @@ public class Controller extends Thread {
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_NAMES request for unknown account %s", peer, address));
- // We'll send empty block summaries message as it's very short
- Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+ // Send generic 'unknown' message as it's very short
+ Message accountUnknownMessage = new GenericUnknownMessage();
accountUnknownMessage.setId(message.getId());
if (!peer.sendMessage(accountUnknownMessage))
peer.disconnect("failed to send account-unknown response");
@@ -1742,8 +1794,8 @@ public class Controller extends Thread {
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
LOGGER.debug(() -> String.format("Sending 'name unknown' response to peer %s for GET_NAME request for unknown name %s", peer, name));
- // We'll send empty block summaries message as it's very short
- Message nameUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+ // Send generic 'unknown' message as it's very short
+ Message nameUnknownMessage = new GenericUnknownMessage();
nameUnknownMessage.setId(message.getId());
if (!peer.sendMessage(nameUnknownMessage))
peer.disconnect("failed to send name-unknown response");
@@ -1791,14 +1843,14 @@ public class Controller extends Thread {
continue;
}
- final PeerChainTipData peerChainTipData = peer.getChainTipData();
+ BlockSummaryData peerChainTipData = peer.getChainTipData();
if (peerChainTipData == null) {
iterator.remove();
continue;
}
// Disregard peers that don't have a recent block
- if (peerChainTipData.getLastBlockTimestamp() == null || peerChainTipData.getLastBlockTimestamp() < minLatestBlockTimestamp) {
+ if (peerChainTipData.getTimestamp() == null || peerChainTipData.getTimestamp() < minLatestBlockTimestamp) {
iterator.remove();
continue;
}
@@ -1826,6 +1878,10 @@ public class Controller extends Thread {
if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp)
return false;
+ if (Settings.getInstance().isSingleNodeTestnet())
+ // Single node testnets won't have peers, so we can assume up to date from this point
+ return true;
+
// Needs a mutable copy of the unmodifiableList
List peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
if (peers == null)
diff --git a/src/main/java/org/qortal/controller/OnlineAccountsManager.java b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
index 254d6168..fd2c38df 100644
--- a/src/main/java/org/qortal/controller/OnlineAccountsManager.java
+++ b/src/main/java/org/qortal/controller/OnlineAccountsManager.java
@@ -53,17 +53,30 @@ public class OnlineAccountsManager {
*/
private static final int MAX_BLOCKS_CACHED_ONLINE_ACCOUNTS = 3;
- private static final long ONLINE_ACCOUNTS_QUEUE_INTERVAL = 100L; //ms
+ private static final long ONLINE_ACCOUNTS_QUEUE_INTERVAL = 100L; // ms
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000L; // ms
- private static final long ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL = 60 * 1000L; // ms
- private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 5 * 1000L; // ms
+ private static final long ONLINE_ACCOUNTS_COMPUTE_INTERVAL = 5 * 1000L; // ms
+ private static final long ONLINE_ACCOUNTS_BROADCAST_INTERVAL = 60 * 1000L; // ms
+ // After switching to a new online timestamp, we "burst" the online accounts requests
+ // at an increased interval for a specified amount of time
+ private static final long ONLINE_ACCOUNTS_BROADCAST_BURST_INTERVAL = 5 * 1000L; // ms
+ private static final long ONLINE_ACCOUNTS_BROADCAST_BURST_LENGTH = 5 * 60 * 1000L; // ms
- private static final long ONLINE_ACCOUNTS_V2_PEER_VERSION = 0x0300020000L; // v3.2.0
- private static final long ONLINE_ACCOUNTS_V3_PEER_VERSION = 0x0300040000L; // v3.4.0
+ private static final long ONLINE_ACCOUNTS_COMPUTE_INITIAL_SLEEP_INTERVAL = 30 * 1000L; // ms
- // MemoryPoW
- public final int POW_BUFFER_SIZE = 1 * 1024 * 1024; // bytes
- public int POW_DIFFICULTY = 18; // leading zero bits
+ // MemoryPoW - mainnet
+ public static final int POW_BUFFER_SIZE = 1 * 1024 * 1024; // bytes
+ public static final int POW_DIFFICULTY_V1 = 18; // leading zero bits
+ public static final int POW_DIFFICULTY_V2 = 19; // leading zero bits
+
+ // MemoryPoW - testnet
+ public static final int POW_BUFFER_SIZE_TESTNET = 1 * 1024 * 1024; // bytes
+ public static final int POW_DIFFICULTY_TESTNET = 5; // leading zero bits
+
+ // IMPORTANT: if we ever need to dynamically modify the buffer size using a feature trigger, the
+ // pre-allocated buffer below will NOT work, and we should instead use a dynamically allocated
+ // one for the transition period.
+ private static long[] POW_VERIFY_WORK_BUFFER = new long[getPoWBufferSize() / 8];
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4, new NamedThreadFactory("OnlineAccounts"));
private volatile boolean isStopping = false;
@@ -85,6 +98,8 @@ public class OnlineAccountsManager {
*/
private final SortedMap> latestBlocksOnlineAccounts = new ConcurrentSkipListMap<>();
+ private long lastOnlineAccountsRequest = 0;
+
private boolean hasOurOnlineAccounts = false;
public static long getOnlineTimestampModulus() {
@@ -107,6 +122,23 @@ public class OnlineAccountsManager {
return (timestamp / getOnlineTimestampModulus()) * getOnlineTimestampModulus();
}
+ private static int getPoWBufferSize() {
+ if (Settings.getInstance().isTestNet())
+ return POW_BUFFER_SIZE_TESTNET;
+
+ return POW_BUFFER_SIZE;
+ }
+
+ private static int getPoWDifficulty(long timestamp) {
+ if (Settings.getInstance().isTestNet())
+ return POW_DIFFICULTY_TESTNET;
+
+ if (timestamp >= BlockChain.getInstance().getIncreaseOnlineAccountsDifficultyTimestamp())
+ return POW_DIFFICULTY_V2;
+
+ return POW_DIFFICULTY_V1;
+ }
+
private OnlineAccountsManager() {
}
@@ -122,16 +154,16 @@ public class OnlineAccountsManager {
// Expire old online accounts signatures
executor.scheduleAtFixedRate(this::expireOldOnlineAccounts, ONLINE_ACCOUNTS_TASKS_INTERVAL, ONLINE_ACCOUNTS_TASKS_INTERVAL, TimeUnit.MILLISECONDS);
- // Send our online accounts
- executor.scheduleAtFixedRate(this::sendOurOnlineAccountsInfo, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
-
- // Request online accounts from peers (legacy)
- executor.scheduleAtFixedRate(this::requestLegacyRemoteOnlineAccounts, ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_LEGACY_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
- // Request online accounts from peers (V3+)
- executor.scheduleAtFixedRate(this::requestRemoteOnlineAccounts, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, ONLINE_ACCOUNTS_BROADCAST_INTERVAL, TimeUnit.MILLISECONDS);
+ // Request online accounts from peers
+ executor.scheduleAtFixedRate(this::requestRemoteOnlineAccounts, ONLINE_ACCOUNTS_BROADCAST_BURST_INTERVAL, ONLINE_ACCOUNTS_BROADCAST_BURST_INTERVAL, TimeUnit.MILLISECONDS);
// Process import queue
executor.scheduleWithFixedDelay(this::processOnlineAccountsImportQueue, ONLINE_ACCOUNTS_QUEUE_INTERVAL, ONLINE_ACCOUNTS_QUEUE_INTERVAL, TimeUnit.MILLISECONDS);
+
+ // Send our online accounts (using increased initial delay)
+ // This allows some time for initial online account lists to be retrieved, and
+ // reduces the chances of the same nonce being computed twice
+ executor.scheduleAtFixedRate(this::sendOurOnlineAccountsInfo, ONLINE_ACCOUNTS_COMPUTE_INITIAL_SLEEP_INTERVAL, ONLINE_ACCOUNTS_COMPUTE_INTERVAL, TimeUnit.MILLISECONDS);
}
public void shutdown() {
@@ -151,7 +183,6 @@ public class OnlineAccountsManager {
return;
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
- final boolean mempowActive = onlineAccountsTimestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp();
Set replacementAccounts = new HashSet<>();
for (PrivateKeyAccount onlineAccount : onlineAccounts) {
@@ -160,7 +191,7 @@ public class OnlineAccountsManager {
byte[] signature = Qortal25519Extras.signForAggregation(onlineAccount.getPrivateKey(), timestampBytes);
byte[] publicKey = onlineAccount.getPublicKey();
- Integer nonce = mempowActive ? new Random().nextInt(500000) : null;
+ Integer nonce = new Random().nextInt(500000);
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, nonce);
replacementAccounts.add(ourOnlineAccountData);
@@ -180,25 +211,37 @@ public class OnlineAccountsManager {
LOGGER.debug("Processing online accounts import queue (size: {})", this.onlineAccountsImportQueue.size());
Set onlineAccountsToAdd = new HashSet<>();
+ Set onlineAccountsToRemove = new HashSet<>();
try (final Repository repository = RepositoryManager.getRepository()) {
for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) {
if (isStopping)
return;
+ // Skip this account if it's already validated
+ Set onlineAccounts = this.currentOnlineAccounts.get(onlineAccountData.getTimestamp());
+ if (onlineAccounts != null && onlineAccounts.contains(onlineAccountData)) {
+ // We have already validated this online account
+ onlineAccountsImportQueue.remove(onlineAccountData);
+ continue;
+ }
+
boolean isValid = this.isValidCurrentAccount(repository, onlineAccountData);
if (isValid)
onlineAccountsToAdd.add(onlineAccountData);
- // Remove from queue
- onlineAccountsImportQueue.remove(onlineAccountData);
+ // Don't remove from the queue yet - we'll do this at the end of the process
+ // This prevents duplicates being added to the queue whilst it's being processed
+ onlineAccountsToRemove.add(onlineAccountData);
}
} catch (DataException e) {
LOGGER.error("Repository issue while verifying online accounts", e);
- }
- if (!onlineAccountsToAdd.isEmpty()) {
- LOGGER.debug("Merging {} validated online accounts from import queue", onlineAccountsToAdd.size());
- addAccounts(onlineAccountsToAdd);
+ } finally {
+ if (!onlineAccountsToAdd.isEmpty()) {
+ LOGGER.debug("Merging {} validated online accounts from import queue", onlineAccountsToAdd.size());
+ addAccounts(onlineAccountsToAdd);
+ }
+ onlineAccountsImportQueue.removeAll(onlineAccountsToRemove);
}
}
@@ -304,12 +347,10 @@ public class OnlineAccountsManager {
return false;
}
- // Validate mempow if feature trigger is active
- if (now >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- if (!getInstance().verifyMemoryPoW(onlineAccountData, now)) {
- LOGGER.trace(() -> String.format("Rejecting online reward-share for account %s due to invalid PoW nonce", mintingAccount.getAddress()));
- return false;
- }
+ // Validate mempow
+ if (!getInstance().verifyMemoryPoW(onlineAccountData, POW_VERIFY_WORK_BUFFER)) {
+ LOGGER.trace(() -> String.format("Rejecting online reward-share for account %s due to invalid PoW nonce", mintingAccount.getAddress()));
+ return false;
}
return true;
@@ -333,7 +374,7 @@ public class OnlineAccountsManager {
for (var entry : hashesToRebuild.entrySet()) {
Long timestamp = entry.getKey();
- LOGGER.debug(() -> String.format("Rehashing for timestamp %d and leading bytes %s",
+ LOGGER.trace(() -> String.format("Rehashing for timestamp %d and leading bytes %s",
timestamp,
entry.getValue().stream().sorted(Byte::compareUnsigned).map(leadingByte -> String.format("%02x", leadingByte)).collect(Collectors.joining(", "))
)
@@ -359,7 +400,7 @@ public class OnlineAccountsManager {
}
}
- LOGGER.debug(String.format("we have online accounts for timestamps: %s", String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
+ LOGGER.trace(String.format("we have online accounts for timestamps: %s", String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
return true;
}
@@ -399,30 +440,7 @@ public class OnlineAccountsManager {
}
/**
- * Request data from other peers. (Pre-V3)
- */
- private void requestLegacyRemoteOnlineAccounts() {
- final Long now = NTP.getTime();
- if (now == null)
- return;
-
- // Don't bother if we're not up to date
- if (!Controller.getInstance().isUpToDate())
- return;
-
- List mergedOnlineAccounts = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
-
- Message messageV2 = new GetOnlineAccountsV2Message(mergedOnlineAccounts);
-
- Network.getInstance().broadcast(peer ->
- peer.getPeersVersion() < ONLINE_ACCOUNTS_V3_PEER_VERSION
- ? messageV2
- : null
- );
- }
-
- /**
- * Request data from other peers. V3+
+ * Request data from other peers
*/
private void requestRemoteOnlineAccounts() {
final Long now = NTP.getTime();
@@ -433,13 +451,25 @@ public class OnlineAccountsManager {
if (!Controller.getInstance().isUpToDate())
return;
- Message messageV3 = new GetOnlineAccountsV3Message(currentOnlineAccountsHashes);
+ long onlineAccountsTimestamp = getCurrentOnlineAccountTimestamp();
+ if (now - onlineAccountsTimestamp >= ONLINE_ACCOUNTS_BROADCAST_BURST_LENGTH) {
+ // New online timestamp started more than 5 mins ago - we probably don't need to request so frequently
- Network.getInstance().broadcast(peer ->
- peer.getPeersVersion() >= ONLINE_ACCOUNTS_V3_PEER_VERSION
- ? messageV3
- : null
- );
+ if (Controller.uptime() < ONLINE_ACCOUNTS_BROADCAST_BURST_LENGTH) {
+ // The node recently started up, so we should request at the burst interval
+ // This could allow accounts to move around the network more easily when an auto update is occurring
+ }
+ else if (now - lastOnlineAccountsRequest < ONLINE_ACCOUNTS_BROADCAST_INTERVAL) {
+ // We already requested online accounts in the last minute, so no need to request again
+ return;
+ }
+ }
+
+ LOGGER.debug("Requesting online accounts via broadcast...");
+
+ lastOnlineAccountsRequest = now;
+ Message messageV3 = new GetOnlineAccountsV3Message(currentOnlineAccountsHashes);
+ Network.getInstance().broadcast(peer -> messageV3);
}
/**
@@ -464,12 +494,10 @@ public class OnlineAccountsManager {
// 'next' timestamp (prioritize this as it's the most important, if mempow active)
final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(now) + getOnlineTimestampModulus();
- if (isMemoryPoWActive(now)) {
- boolean success = computeOurAccountsForTimestamp(nextOnlineAccountsTimestamp);
- if (!success) {
- // We didn't compute the required nonce value(s), and so can't proceed until they have been retried
- return;
- }
+ boolean success = computeOurAccountsForTimestamp(nextOnlineAccountsTimestamp);
+ if (!success) {
+ // We didn't compute the required nonce value(s), and so can't proceed until they have been retried
+ return;
}
// 'current' timestamp
@@ -522,6 +550,8 @@ public class OnlineAccountsManager {
Set onlineAccounts = this.currentOnlineAccounts.computeIfAbsent(onlineAccountsTimestamp, k -> ConcurrentHashMap.newKeySet());
boolean alreadyExists = onlineAccounts.stream().anyMatch(a -> Arrays.equals(a.getPublicKey(), publicKey));
if (alreadyExists) {
+ this.hasOurOnlineAccounts = true;
+
if (remaining > 0) {
// Move on to next account
continue;
@@ -544,21 +574,15 @@ public class OnlineAccountsManager {
// Compute nonce
Integer nonce;
- if (isMemoryPoWActive(NTP.getTime())) {
- try {
- nonce = this.computeMemoryPoW(mempowBytes, publicKey, onlineAccountsTimestamp);
- if (nonce == null) {
- // A nonce is required
- return false;
- }
- } catch (TimeoutException e) {
- LOGGER.info(String.format("Timed out computing nonce for account %.8s", Base58.encode(publicKey)));
+ try {
+ nonce = this.computeMemoryPoW(mempowBytes, publicKey, onlineAccountsTimestamp);
+ if (nonce == null) {
+ // A nonce is required
return false;
}
- }
- else {
- // Send -1 if we haven't computed a nonce due to feature trigger timestamp
- nonce = -1;
+ } catch (TimeoutException e) {
+ LOGGER.info(String.format("Timed out computing nonce for account %.8s", Base58.encode(publicKey)));
+ return false;
}
byte[] signature = Qortal25519Extras.signForAggregation(privateKey, timestampBytes);
@@ -567,7 +591,7 @@ public class OnlineAccountsManager {
OnlineAccountData ourOnlineAccountData = new OnlineAccountData(onlineAccountsTimestamp, signature, publicKey, nonce);
// Make sure to verify before adding
- if (verifyMemoryPoW(ourOnlineAccountData, NTP.getTime())) {
+ if (verifyMemoryPoW(ourOnlineAccountData, null)) {
ourOnlineAccounts.add(ourOnlineAccountData);
}
}
@@ -579,17 +603,7 @@ public class OnlineAccountsManager {
if (!hasInfoChanged)
return false;
- Message messageV1 = new OnlineAccountsMessage(ourOnlineAccounts);
- Message messageV2 = new OnlineAccountsV2Message(ourOnlineAccounts);
- Message messageV3 = new OnlineAccountsV3Message(ourOnlineAccounts);
-
- Network.getInstance().broadcast(peer ->
- peer.getPeersVersion() >= OnlineAccountsV3Message.MIN_PEER_VERSION
- ? messageV3
- : peer.getPeersVersion() >= ONLINE_ACCOUNTS_V2_PEER_VERSION
- ? messageV2
- : messageV1
- );
+ Network.getInstance().broadcast(peer -> new OnlineAccountsV3Message(ourOnlineAccounts));
LOGGER.debug("Broadcasted {} online account{} with timestamp {}", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp);
@@ -600,12 +614,6 @@ public class OnlineAccountsManager {
// MemoryPoW
- private boolean isMemoryPoWActive(Long timestamp) {
- if (timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp() || Settings.getInstance().isOnlineAccountsMemPoWEnabled()) {
- return true;
- }
- return false;
- }
private byte[] getMemoryPoWBytes(byte[] publicKey, long onlineAccountsTimestamp) throws IOException {
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
@@ -617,11 +625,6 @@ public class OnlineAccountsManager {
}
private Integer computeMemoryPoW(byte[] bytes, byte[] publicKey, long onlineAccountsTimestamp) throws TimeoutException {
- if (!isMemoryPoWActive(NTP.getTime())) {
- LOGGER.info("Mempow start timestamp not yet reached, and onlineAccountsMemPoWEnabled not enabled in settings");
- return null;
- }
-
LOGGER.info(String.format("Computing nonce for account %.8s and timestamp %d...", Base58.encode(publicKey), onlineAccountsTimestamp));
// Calculate the time until the next online timestamp and use it as a timeout when computing the nonce
@@ -629,7 +632,8 @@ public class OnlineAccountsManager {
final long nextOnlineAccountsTimestamp = toOnlineAccountTimestamp(startTime) + getOnlineTimestampModulus();
long timeUntilNextTimestamp = nextOnlineAccountsTimestamp - startTime;
- Integer nonce = MemoryPoW.compute2(bytes, POW_BUFFER_SIZE, POW_DIFFICULTY, timeUntilNextTimestamp);
+ int difficulty = getPoWDifficulty(onlineAccountsTimestamp);
+ Integer nonce = MemoryPoW.compute2(bytes, getPoWBufferSize(), difficulty, timeUntilNextTimestamp);
double totalSeconds = (NTP.getTime() - startTime) / 1000.0f;
int minutes = (int) ((totalSeconds % 3600) / 60);
@@ -638,15 +642,15 @@ public class OnlineAccountsManager {
LOGGER.info(String.format("Computed nonce for timestamp %d and account %.8s: %d. Buffer size: %d. Difficulty: %d. " +
"Time taken: %02d:%02d. Hashrate: %f", onlineAccountsTimestamp, Base58.encode(publicKey),
- nonce, POW_BUFFER_SIZE, POW_DIFFICULTY, minutes, seconds, hashRate));
+ nonce, getPoWBufferSize(), difficulty, minutes, seconds, hashRate));
return nonce;
}
- public boolean verifyMemoryPoW(OnlineAccountData onlineAccountData, Long timestamp) {
- if (!isMemoryPoWActive(timestamp)) {
- // Not active yet, so treat it as valid
- return true;
+ public boolean verifyMemoryPoW(OnlineAccountData onlineAccountData, long[] workBuffer) {
+ // Require a valid nonce value
+ if (onlineAccountData.getNonce() == null || onlineAccountData.getNonce() < 0) {
+ return false;
}
int nonce = onlineAccountData.getNonce();
@@ -659,7 +663,7 @@ public class OnlineAccountsManager {
}
// Verify the nonce
- return MemoryPoW.verify2(mempowBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
+ return MemoryPoW.verify2(mempowBytes, workBuffer, getPoWBufferSize(), getPoWDifficulty(onlineAccountData.getTimestamp()), nonce);
}
@@ -697,7 +701,7 @@ public class OnlineAccountsManager {
*/
// Block::mint() - only wants online accounts with (online) timestamp that matches block's (online) timestamp so they can be added to new block
public List getOnlineAccounts(long onlineTimestamp) {
- LOGGER.info(String.format("caller's timestamp: %d, our timestamps: %s", onlineTimestamp, String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
+ LOGGER.debug(String.format("caller's timestamp: %d, our timestamps: %s", onlineTimestamp, String.join(", ", this.currentOnlineAccounts.keySet().stream().map(l -> Long.toString(l)).collect(Collectors.joining(", ")))));
return new ArrayList<>(Set.copyOf(this.currentOnlineAccounts.getOrDefault(onlineTimestamp, Collections.emptySet())));
}
@@ -743,11 +747,12 @@ public class OnlineAccountsManager {
* Typically called by {@link Block#areOnlineAccountsValid()}
*/
public void addBlocksOnlineAccounts(Set blocksOnlineAccounts, Long timestamp) {
- // We want to add to 'current' in preference if possible
- if (this.currentOnlineAccounts.containsKey(timestamp)) {
- addAccounts(blocksOnlineAccounts);
+ // If these are current accounts, then there is no need to cache them, and should instead rely
+ // on the more complete entries we already have in self.currentOnlineAccounts.
+ // Note: since sig-agg, we no longer have individual signatures included in blocks, so we
+ // mustn't add anything to currentOnlineAccounts from here.
+ if (this.currentOnlineAccounts.containsKey(timestamp))
return;
- }
// Add to block cache instead
this.latestBlocksOnlineAccounts.computeIfAbsent(timestamp, k -> ConcurrentHashMap.newKeySet())
@@ -767,106 +772,6 @@ public class OnlineAccountsManager {
// Network handlers
- public void onNetworkGetOnlineAccountsMessage(Peer peer, Message message) {
- GetOnlineAccountsMessage getOnlineAccountsMessage = (GetOnlineAccountsMessage) message;
-
- List excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
-
- // Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
- List accountsToSend = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
- int prefilterSize = accountsToSend.size();
-
- Iterator iterator = accountsToSend.iterator();
- while (iterator.hasNext()) {
- OnlineAccountData onlineAccountData = iterator.next();
-
- for (OnlineAccountData excludeAccountData : excludeAccounts) {
- if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
- iterator.remove();
- break;
- }
- }
- }
-
- if (accountsToSend.isEmpty())
- return;
-
- Message onlineAccountsMessage = new OnlineAccountsMessage(accountsToSend);
- peer.sendMessage(onlineAccountsMessage);
-
- LOGGER.debug("Sent {} of our {} online accounts to {}", accountsToSend.size(), prefilterSize, peer);
- }
-
- public void onNetworkOnlineAccountsMessage(Peer peer, Message message) {
- OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message;
-
- List peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
- LOGGER.debug("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
-
- int importCount = 0;
-
- // Add any online accounts to the queue that aren't already present
- for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
- boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
-
- if (isNewEntry)
- importCount++;
- }
-
- if (importCount > 0)
- LOGGER.debug("Added {} online accounts to queue", importCount);
- }
-
- public void onNetworkGetOnlineAccountsV2Message(Peer peer, Message message) {
- GetOnlineAccountsV2Message getOnlineAccountsMessage = (GetOnlineAccountsV2Message) message;
-
- List excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
-
- // Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
- List accountsToSend = Set.copyOf(this.currentOnlineAccounts.values()).stream().flatMap(Set::stream).collect(Collectors.toList());
- int prefilterSize = accountsToSend.size();
-
- Iterator iterator = accountsToSend.iterator();
- while (iterator.hasNext()) {
- OnlineAccountData onlineAccountData = iterator.next();
-
- for (OnlineAccountData excludeAccountData : excludeAccounts) {
- if (onlineAccountData.getTimestamp() == excludeAccountData.getTimestamp() && Arrays.equals(onlineAccountData.getPublicKey(), excludeAccountData.getPublicKey())) {
- iterator.remove();
- break;
- }
- }
- }
-
- if (accountsToSend.isEmpty())
- return;
-
- Message onlineAccountsMessage = new OnlineAccountsV2Message(accountsToSend);
- peer.sendMessage(onlineAccountsMessage);
-
- LOGGER.debug("Sent {} of our {} online accounts to {}", accountsToSend.size(), prefilterSize, peer);
- }
-
- public void onNetworkOnlineAccountsV2Message(Peer peer, Message message) {
- OnlineAccountsV2Message onlineAccountsMessage = (OnlineAccountsV2Message) message;
-
- List peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
- LOGGER.debug("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
-
- int importCount = 0;
-
- // Add any online accounts to the queue that aren't already present
- for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
- boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
-
- if (isNewEntry)
- importCount++;
- }
-
- if (importCount > 0)
- LOGGER.debug("Added {} online accounts to queue", importCount);
- }
-
public void onNetworkGetOnlineAccountsV3Message(Peer peer, Message message) {
GetOnlineAccountsV3Message getOnlineAccountsMessage = (GetOnlineAccountsV3Message) message;
@@ -887,7 +792,7 @@ public class OnlineAccountsManager {
Set timestampsOnlineAccounts = this.currentOnlineAccounts.getOrDefault(timestamp, Collections.emptySet());
outgoingOnlineAccounts.addAll(timestampsOnlineAccounts);
- LOGGER.debug(() -> String.format("Going to send all %d online accounts for timestamp %d", timestampsOnlineAccounts.size(), timestamp));
+ LOGGER.trace(() -> String.format("Going to send all %d online accounts for timestamp %d", timestampsOnlineAccounts.size(), timestamp));
} else {
// Quick cache of which leading bytes to send so we only have to filter once
Set outgoingLeadingBytes = new HashSet<>();
@@ -911,7 +816,7 @@ public class OnlineAccountsManager {
.forEach(outgoingOnlineAccounts::add);
if (outgoingOnlineAccounts.size() > beforeAddSize)
- LOGGER.debug(String.format("Going to send %d online accounts for timestamp %d and leading bytes %s",
+ LOGGER.trace(String.format("Going to send %d online accounts for timestamp %d and leading bytes %s",
outgoingOnlineAccounts.size() - beforeAddSize,
timestamp,
outgoingLeadingBytes.stream().sorted(Byte::compareUnsigned).map(leadingByte -> String.format("%02x", leadingByte)).collect(Collectors.joining(", "))
@@ -920,25 +825,27 @@ public class OnlineAccountsManager {
}
}
- peer.sendMessage(
- peer.getPeersVersion() >= OnlineAccountsV3Message.MIN_PEER_VERSION ?
- new OnlineAccountsV3Message(outgoingOnlineAccounts) :
- new OnlineAccountsV2Message(outgoingOnlineAccounts)
- );
+ peer.sendMessage(new OnlineAccountsV3Message(outgoingOnlineAccounts));
- LOGGER.debug("Sent {} online accounts to {}", outgoingOnlineAccounts.size(), peer);
+ LOGGER.trace("Sent {} online accounts to {}", outgoingOnlineAccounts.size(), peer);
}
public void onNetworkOnlineAccountsV3Message(Peer peer, Message message) {
OnlineAccountsV3Message onlineAccountsMessage = (OnlineAccountsV3Message) message;
List peersOnlineAccounts = onlineAccountsMessage.getOnlineAccounts();
- LOGGER.debug("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
+ LOGGER.trace("Received {} online accounts from {}", peersOnlineAccounts.size(), peer);
int importCount = 0;
// Add any online accounts to the queue that aren't already present
for (OnlineAccountData onlineAccountData : peersOnlineAccounts) {
+
+ Set onlineAccounts = this.currentOnlineAccounts.computeIfAbsent(onlineAccountData.getTimestamp(), k -> ConcurrentHashMap.newKeySet());
+ if (onlineAccounts.contains(onlineAccountData))
+ // We have already validated this online account
+ continue;
+
boolean isNewEntry = onlineAccountsImportQueue.add(onlineAccountData);
if (isNewEntry)
diff --git a/src/main/java/org/qortal/controller/PirateChainWalletController.java b/src/main/java/org/qortal/controller/PirateChainWalletController.java
index 1eac4b3a..333c2cda 100644
--- a/src/main/java/org/qortal/controller/PirateChainWalletController.java
+++ b/src/main/java/org/qortal/controller/PirateChainWalletController.java
@@ -4,6 +4,7 @@ import com.rust.litewalletjni.LiteWalletJni;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.json.JSONException;
import org.json.JSONObject;
import org.qortal.arbitrary.ArbitraryDataFile;
import org.qortal.arbitrary.ArbitraryDataReader;
@@ -99,14 +100,19 @@ public class PirateChainWalletController extends Thread {
LOGGER.debug("Syncing Pirate Chain wallet...");
String response = LiteWalletJni.execute("sync", "");
LOGGER.debug("sync response: {}", response);
- JSONObject json = new JSONObject(response);
- if (json.has("result")) {
- String result = json.getString("result");
- // We may have to set wallet to ready if this is the first ever successful sync
- if (Objects.equals(result, "success")) {
- this.currentWallet.setReady(true);
+ try {
+ JSONObject json = new JSONObject(response);
+ if (json.has("result")) {
+ String result = json.getString("result");
+
+ // We may have to set wallet to ready if this is the first ever successful sync
+ if (Objects.equals(result, "success")) {
+ this.currentWallet.setReady(true);
+ }
}
+ } catch (JSONException e) {
+ LOGGER.info("Unable to interpret JSON", e);
}
// Rate limit sync attempts
diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java
index 74a4a785..cd9483e9 100644
--- a/src/main/java/org/qortal/controller/Synchronizer.java
+++ b/src/main/java/org/qortal/controller/Synchronizer.java
@@ -19,7 +19,6 @@ import org.qortal.block.BlockChain;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.block.CommonBlockData;
-import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.transaction.RewardShareTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.event.Event;
@@ -54,7 +53,8 @@ public class Synchronizer extends Thread {
/** Maximum number of block signatures we ask from peer in one go */
private static final int MAXIMUM_REQUEST_SIZE = 200; // XXX move to Settings?
- private static final long RECOVERY_MODE_TIMEOUT = 10 * 60 * 1000L; // ms
+ /** Maximum number of consecutive failed sync attempts before marking peer as misbehaved */
+ private static final int MAX_CONSECUTIVE_FAILED_SYNC_ATTEMPTS = 3;
private boolean running;
@@ -76,6 +76,8 @@ public class Synchronizer extends Thread {
private volatile boolean isSynchronizing = false;
/** Temporary estimate of synchronization progress for SysTray use. */
private volatile int syncPercent = 0;
+ /** Temporary estimate of blocks remaining for SysTray use. */
+ private volatile int blocksRemaining = 0;
private static volatile boolean requestSync = false;
private boolean syncRequestPending = false;
@@ -181,6 +183,18 @@ public class Synchronizer extends Thread {
}
}
+ public Integer getBlocksRemaining() {
+ synchronized (this.syncLock) {
+ // Report as 0 blocks remaining if the latest block is within the last 60 mins
+ final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
+ if (Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) {
+ return 0;
+ }
+
+ return this.isSynchronizing ? this.blocksRemaining : null;
+ }
+ }
+
public void requestSync() {
requestSync = true;
}
@@ -282,7 +296,7 @@ public class Synchronizer extends Thread {
BlockData priorChainTip = Controller.getInstance().getChainTip();
synchronized (this.syncLock) {
- this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
+ this.syncPercent = (priorChainTip.getHeight() * 100) / peer.getChainTipData().getHeight();
// Only update SysTray if we're potentially changing height
if (this.syncPercent < 100) {
@@ -312,7 +326,7 @@ public class Synchronizer extends Thread {
case INFERIOR_CHAIN: {
// Update our list of inferior chain tips
- ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
+ ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getSignature());
if (!inferiorChainSignatures.contains(inferiorChainSignature))
inferiorChainSignatures.add(inferiorChainSignature);
@@ -320,7 +334,8 @@ public class Synchronizer extends Thread {
LOGGER.debug(() -> String.format("Refused to synchronize with peer %s (%s)", peer, syncResult.name()));
// Notify peer of our superior chain
- if (!peer.sendMessage(Network.getInstance().buildHeightMessage(peer, priorChainTip)))
+ Message message = Network.getInstance().buildHeightOrChainTipInfo(peer);
+ if (message == null || !peer.sendMessage(message))
peer.disconnect("failed to notify peer of our superior chain");
break;
}
@@ -341,7 +356,7 @@ public class Synchronizer extends Thread {
// fall-through...
case NOTHING_TO_DO: {
// Update our list of inferior chain tips
- ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getLastBlockSignature());
+ ByteArray inferiorChainSignature = ByteArray.wrap(peer.getChainTipData().getSignature());
if (!inferiorChainSignatures.contains(inferiorChainSignature))
inferiorChainSignatures.add(inferiorChainSignature);
@@ -369,8 +384,7 @@ public class Synchronizer extends Thread {
// Reset our cache of inferior chains
inferiorChainSignatures.clear();
- Network network = Network.getInstance();
- network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newChainTip));
+ Network.getInstance().broadcastOurChain();
EventBus.INSTANCE.notify(new NewChainTipEvent(priorChainTip, newChainTip));
}
@@ -397,9 +411,10 @@ public class Synchronizer extends Thread {
timePeersLastAvailable = NTP.getTime();
// If enough time has passed, enter recovery mode, which lifts some restrictions on who we can sync with and when we can mint
- if (NTP.getTime() - timePeersLastAvailable > RECOVERY_MODE_TIMEOUT) {
+ long recoveryModeTimeout = Settings.getInstance().getRecoveryModeTimeout();
+ if (NTP.getTime() - timePeersLastAvailable > recoveryModeTimeout) {
if (recoveryMode == false) {
- LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", RECOVERY_MODE_TIMEOUT/60/1000));
+ LOGGER.info(String.format("Peers have been unavailable for %d minutes. Entering recovery mode...", recoveryModeTimeout/60/1000));
recoveryMode = true;
}
}
@@ -513,13 +528,13 @@ public class Synchronizer extends Thread {
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
final int ourInitialHeight = ourLatestBlockData.getHeight();
- PeerChainTipData peerChainTipData = peer.getChainTipData();
- int peerHeight = peerChainTipData.getLastHeight();
- byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
+ BlockSummaryData peerChainTipData = peer.getChainTipData();
+ int peerHeight = peerChainTipData.getHeight();
+ byte[] peersLastBlockSignature = peerChainTipData.getSignature();
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
LOGGER.debug(String.format("Fetching summaries from peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
- peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
+ peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp()));
List peerBlockSummaries = new ArrayList<>();
@@ -637,9 +652,9 @@ public class Synchronizer extends Thread {
return peers;
// Count the number of blocks this peer has beyond our common block
- final PeerChainTipData peerChainTipData = peer.getChainTipData();
- final int peerHeight = peerChainTipData.getLastHeight();
- final byte[] peerLastBlockSignature = peerChainTipData.getLastBlockSignature();
+ final BlockSummaryData peerChainTipData = peer.getChainTipData();
+ final int peerHeight = peerChainTipData.getHeight();
+ final byte[] peerLastBlockSignature = peerChainTipData.getSignature();
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
// Limit the number of blocks we are comparing. FUTURE: we could request more in batches, but there may not be a case when this is needed
int summariesRequired = Math.min(peerAdditionalBlocksAfterCommonBlock, MAXIMUM_REQUEST_SIZE);
@@ -727,8 +742,9 @@ public class Synchronizer extends Thread {
LOGGER.debug(String.format("Listing peers with common block %.8s...", Base58.encode(commonBlockSummary.getSignature())));
for (Peer peer : peersSharingCommonBlock) {
- final int peerHeight = peer.getChainTipData().getLastHeight();
- final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
+ BlockSummaryData peerChainTipData = peer.getChainTipData();
+ final int peerHeight = peerChainTipData.getHeight();
+ final Long peerLastBlockTimestamp = peerChainTipData.getTimestamp();
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
final CommonBlockData peerCommonBlockData = peer.getCommonBlockData();
@@ -825,7 +841,7 @@ public class Synchronizer extends Thread {
// Calculate the length of the shortest peer chain sharing this common block
int minChainLength = 0;
for (Peer peer : peersSharingCommonBlock) {
- final int peerHeight = peer.getChainTipData().getLastHeight();
+ final int peerHeight = peer.getChainTipData().getHeight();
final int peerAdditionalBlocksAfterCommonBlock = peerHeight - commonBlockSummary.getHeight();
if (peerAdditionalBlocksAfterCommonBlock < minChainLength || minChainLength == 0)
@@ -933,13 +949,13 @@ public class Synchronizer extends Thread {
final BlockData ourLatestBlockData = repository.getBlockRepository().getLastBlock();
final int ourInitialHeight = ourLatestBlockData.getHeight();
- PeerChainTipData peerChainTipData = peer.getChainTipData();
- int peerHeight = peerChainTipData.getLastHeight();
- byte[] peersLastBlockSignature = peerChainTipData.getLastBlockSignature();
+ BlockSummaryData peerChainTipData = peer.getChainTipData();
+ int peerHeight = peerChainTipData.getHeight();
+ byte[] peersLastBlockSignature = peerChainTipData.getSignature();
byte[] ourLastBlockSignature = ourLatestBlockData.getSignature();
String syncString = String.format("Synchronizing with peer %s at height %d, sig %.8s, ts %d; our height %d, sig %.8s, ts %d", peer,
- peerHeight, Base58.encode(peersLastBlockSignature), peer.getChainTipData().getLastBlockTimestamp(),
+ peerHeight, Base58.encode(peersLastBlockSignature), peerChainTipData.getTimestamp(),
ourInitialHeight, Base58.encode(ourLastBlockSignature), ourLatestBlockData.getTimestamp());
LOGGER.info(syncString);
@@ -1246,7 +1262,14 @@ public class Synchronizer extends Thread {
int numberSignaturesRequired = additionalPeerBlocksAfterCommonBlock - peerBlockSignatures.size();
int retryCount = 0;
- while (height < peerHeight) {
+
+ // Keep fetching blocks from peer until we reach their tip, or reach a count of MAXIMUM_COMMON_DELTA blocks.
+ // We need to limit the total number, otherwise too much can be loaded into memory, causing an
+ // OutOfMemoryException. This is common when syncing from 1000+ blocks behind the chain tip, after starting
+ // from a small fork that didn't become part of the main chain. This causes the entire sync process to
+ // use syncToPeerChain(), resulting in potentially thousands of blocks being held in memory if the limit
+ // below isn't applied.
+ while (height < peerHeight && peerBlocks.size() <= MAXIMUM_COMMON_DELTA) {
if (Controller.isStopping())
return SynchronizationResult.SHUTTING_DOWN;
@@ -1313,7 +1336,7 @@ public class Synchronizer extends Thread {
// Final check to make sure the peer isn't out of date (except for when we're in recovery mode)
if (!recoveryMode && peer.getChainTipData() != null) {
final Long minLatestBlockTimestamp = Controller.getMinimumLatestBlockTimestamp();
- final Long peerLastBlockTimestamp = peer.getChainTipData().getLastBlockTimestamp();
+ final Long peerLastBlockTimestamp = peer.getChainTipData().getTimestamp();
if (peerLastBlockTimestamp == null || peerLastBlockTimestamp < minLatestBlockTimestamp) {
LOGGER.info(String.format("Peer %s is out of date, so abandoning sync attempt", peer));
return SynchronizationResult.CHAIN_TIP_TOO_OLD;
@@ -1448,6 +1471,12 @@ public class Synchronizer extends Thread {
repository.saveChanges();
+ synchronized (this.syncLock) {
+ if (peer.getChainTipData() != null) {
+ this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
+ }
+ }
+
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
@@ -1543,6 +1572,12 @@ public class Synchronizer extends Thread {
repository.saveChanges();
+ synchronized (this.syncLock) {
+ if (peer.getChainTipData() != null) {
+ this.blocksRemaining = peer.getChainTipData().getHeight() - newBlock.getBlockData().getHeight();
+ }
+ }
+
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
@@ -1553,12 +1588,19 @@ public class Synchronizer extends Thread {
Message getBlockSummariesMessage = new GetBlockSummariesMessage(parentSignature, numberRequested);
Message message = peer.getResponse(getBlockSummariesMessage);
- if (message == null || message.getType() != MessageType.BLOCK_SUMMARIES)
+ if (message == null)
return null;
- BlockSummariesMessage blockSummariesMessage = (BlockSummariesMessage) message;
+ if (message.getType() == MessageType.BLOCK_SUMMARIES) {
+ BlockSummariesMessage blockSummariesMessage = (BlockSummariesMessage) message;
+ return blockSummariesMessage.getBlockSummaries();
+ }
+ else if (message.getType() == MessageType.BLOCK_SUMMARIES_V2) {
+ BlockSummariesV2Message blockSummariesMessage = (BlockSummariesV2Message) message;
+ return blockSummariesMessage.getBlockSummaries();
+ }
- return blockSummariesMessage.getBlockSummaries();
+ return null;
}
private List getBlockSignatures(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
@@ -1577,8 +1619,20 @@ public class Synchronizer extends Thread {
Message getBlockMessage = new GetBlockMessage(signature);
Message message = peer.getResponse(getBlockMessage);
- if (message == null)
+ if (message == null) {
+ peer.getPeerData().incrementFailedSyncCount();
+ if (peer.getPeerData().getFailedSyncCount() >= MAX_CONSECUTIVE_FAILED_SYNC_ATTEMPTS) {
+ // Several failed attempts, so mark peer as misbehaved
+ LOGGER.info("Marking peer {} as misbehaved due to {} failed sync attempts", peer, peer.getPeerData().getFailedSyncCount());
+ Network.getInstance().peerMisbehaved(peer);
+ }
return null;
+ }
+
+ // Reset failed sync count now that we have a block response
+ // FUTURE: we could move this to the end of the sync process, but to reduce risk this can be done
+ // at a later stage. For now we are only defending against serialization errors or no responses.
+ peer.getPeerData().setFailedSyncCount(0);
switch (message.getType()) {
case BLOCK: {
diff --git a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
index 22cf4144..30b0fcca 100644
--- a/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
+++ b/src/main/java/org/qortal/controller/arbitrary/ArbitraryDataFileManager.java
@@ -595,9 +595,10 @@ public class ArbitraryDataFileManager extends Thread {
// Send valid, yet unexpected message type in response, so peer's synchronizer doesn't have to wait for timeout
LOGGER.debug(String.format("Sending 'file unknown' response to peer %s for GET_FILE request for unknown file %s", peer, arbitraryDataFile));
- // We'll send empty block summaries message as it's very short
- // TODO: use a different message type here
- Message fileUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
+ // Send generic 'unknown' message as it's very short
+ Message fileUnknownMessage = peer.getPeersVersion() >= GenericUnknownMessage.MINIMUM_PEER_VERSION
+ ? new GenericUnknownMessage()
+ : new BlockSummariesMessage(Collections.emptyList());
fileUnknownMessage.setId(message.getId());
if (!peer.sendMessage(fileUnknownMessage)) {
LOGGER.debug("Couldn't sent file-unknown response");
diff --git a/src/main/java/org/qortal/controller/repository/BlockArchiver.java b/src/main/java/org/qortal/controller/repository/BlockArchiver.java
index 8757bf32..63d61ef8 100644
--- a/src/main/java/org/qortal/controller/repository/BlockArchiver.java
+++ b/src/main/java/org/qortal/controller/repository/BlockArchiver.java
@@ -16,7 +16,7 @@ public class BlockArchiver implements Runnable {
private static final Logger LOGGER = LogManager.getLogger(BlockArchiver.class);
- private static final long INITIAL_SLEEP_PERIOD = 0L; // TODO: 5 * 60 * 1000L + 1234L; // ms
+ private static final long INITIAL_SLEEP_PERIOD = 5 * 60 * 1000L + 1234L; // ms
public void run() {
Thread.currentThread().setName("Block archiver");
diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
index a31a1a28..a4ae921e 100644
--- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java
@@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
@@ -317,20 +318,27 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+ // Do this in a new thread so caller doesn't have to wait for computeNonce()
+ // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
+ new Thread(() -> {
+ try (final Repository threadsRepository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
- messageTransaction.computeNonce();
- messageTransaction.sign(sender);
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
- // reset repository state to prevent deadlock
- repository.discardChanges();
- ValidationResult result = messageTransaction.importAsUnconfirmed();
+ // reset repository state to prevent deadlock
+ threadsRepository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
- if (result != ValidationResult.OK) {
- LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
- return ResponseResult.NETWORK_ISSUE;
- }
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
+ }
+ } catch (DataException e) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
+ }
+ }, "TradeBot response").start();
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
index c7ae1db3..5880f561 100644
--- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
@@ -468,9 +468,6 @@ public class TradeBot implements Listener {
List safeTradePresences = List.copyOf(this.safeAllTradePresencesByPubkey.values());
- if (safeTradePresences.isEmpty())
- return;
-
LOGGER.debug("Broadcasting all {} known trade presences. Next broadcast timestamp: {}",
safeTradePresences.size(), nextTradePresenceBroadcastTimestamp
);
@@ -637,7 +634,7 @@ public class TradeBot implements Listener {
}
if (newCount > 0) {
- LOGGER.debug("New trade presences: {}", newCount);
+ LOGGER.debug("New trade presences: {}, all trade presences: {}", newCount, allTradePresencesByPubkey.size());
rebuildSafeAllTradePresences();
}
}
diff --git a/src/main/java/org/qortal/crypto/MemoryPoW.java b/src/main/java/org/qortal/crypto/MemoryPoW.java
index f27c8f7a..634b8f9b 100644
--- a/src/main/java/org/qortal/crypto/MemoryPoW.java
+++ b/src/main/java/org/qortal/crypto/MemoryPoW.java
@@ -99,6 +99,10 @@ public class MemoryPoW {
}
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
+ return verify2(data, null, workBufferLength, difficulty, nonce);
+ }
+
+ public static boolean verify2(byte[] data, long[] workBuffer, int workBufferLength, long difficulty, int nonce) {
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
@@ -111,7 +115,10 @@ public class MemoryPoW {
byteBuffer = null;
int longBufferLength = workBufferLength / 8;
- long[] workBuffer = new long[longBufferLength];
+
+ if (workBuffer == null)
+ workBuffer = new long[longBufferLength];
+
long[] state = new long[4];
long seed = 8682522807148012L;
diff --git a/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceMetadata.java b/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceMetadata.java
index 75b5a4d8..e2bcaf56 100644
--- a/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceMetadata.java
+++ b/src/main/java/org/qortal/data/arbitrary/ArbitraryResourceMetadata.java
@@ -24,7 +24,10 @@ public class ArbitraryResourceMetadata {
this.description = description;
this.tags = tags;
this.category = category;
- this.categoryName = category.getName();
+
+ if (category != null) {
+ this.categoryName = category.getName();
+ }
}
public static ArbitraryResourceMetadata fromTransactionMetadata(ArbitraryDataTransactionMetadata transactionMetadata) {
diff --git a/src/main/java/org/qortal/data/block/BlockSummaryData.java b/src/main/java/org/qortal/data/block/BlockSummaryData.java
index 2167f0f0..57e29d0d 100644
--- a/src/main/java/org/qortal/data/block/BlockSummaryData.java
+++ b/src/main/java/org/qortal/data/block/BlockSummaryData.java
@@ -11,11 +11,12 @@ public class BlockSummaryData {
private int height;
private byte[] signature;
private byte[] minterPublicKey;
- private int onlineAccountsCount;
// Optional, set during construction
+ private Integer onlineAccountsCount;
private Long timestamp;
private Integer transactionCount;
+ private byte[] reference;
// Optional, set after construction
private Integer minterLevel;
@@ -25,6 +26,15 @@ public class BlockSummaryData {
protected BlockSummaryData() {
}
+ /** Constructor typically populated with fields from HeightV2Message */
+ public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, long timestamp) {
+ this.height = height;
+ this.signature = signature;
+ this.minterPublicKey = minterPublicKey;
+ this.timestamp = timestamp;
+ }
+
+ /** Constructor typically populated with fields from BlockSummariesMessage */
public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount) {
this.height = height;
this.signature = signature;
@@ -32,13 +42,16 @@ public class BlockSummaryData {
this.onlineAccountsCount = onlineAccountsCount;
}
- public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, int onlineAccountsCount, long timestamp, int transactionCount) {
+ /** Constructor typically populated with fields from BlockSummariesV2Message */
+ public BlockSummaryData(int height, byte[] signature, byte[] minterPublicKey, Integer onlineAccountsCount,
+ Long timestamp, Integer transactionCount, byte[] reference) {
this.height = height;
this.signature = signature;
this.minterPublicKey = minterPublicKey;
this.onlineAccountsCount = onlineAccountsCount;
this.timestamp = timestamp;
this.transactionCount = transactionCount;
+ this.reference = reference;
}
public BlockSummaryData(BlockData blockData) {
@@ -49,6 +62,7 @@ public class BlockSummaryData {
this.timestamp = blockData.getTimestamp();
this.transactionCount = blockData.getTransactionCount();
+ this.reference = blockData.getReference();
}
// Getters / setters
@@ -65,7 +79,7 @@ public class BlockSummaryData {
return this.minterPublicKey;
}
- public int getOnlineAccountsCount() {
+ public Integer getOnlineAccountsCount() {
return this.onlineAccountsCount;
}
@@ -77,6 +91,10 @@ public class BlockSummaryData {
return this.transactionCount;
}
+ public byte[] getReference() {
+ return this.reference;
+ }
+
public Integer getMinterLevel() {
return this.minterLevel;
}
diff --git a/src/main/java/org/qortal/data/block/CommonBlockData.java b/src/main/java/org/qortal/data/block/CommonBlockData.java
index dd502df7..37e9649b 100644
--- a/src/main/java/org/qortal/data/block/CommonBlockData.java
+++ b/src/main/java/org/qortal/data/block/CommonBlockData.java
@@ -1,7 +1,5 @@
package org.qortal.data.block;
-import org.qortal.data.network.PeerChainTipData;
-
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.math.BigInteger;
@@ -14,14 +12,14 @@ public class CommonBlockData {
private BlockSummaryData commonBlockSummary = null;
private List blockSummariesAfterCommonBlock = null;
private BigInteger chainWeight = null;
- private PeerChainTipData chainTipData = null;
+ private BlockSummaryData chainTipData = null;
// Constructors
protected CommonBlockData() {
}
- public CommonBlockData(BlockSummaryData commonBlockSummary, PeerChainTipData chainTipData) {
+ public CommonBlockData(BlockSummaryData commonBlockSummary, BlockSummaryData chainTipData) {
this.commonBlockSummary = commonBlockSummary;
this.chainTipData = chainTipData;
}
@@ -49,7 +47,7 @@ public class CommonBlockData {
this.chainWeight = chainWeight;
}
- public PeerChainTipData getChainTipData() {
+ public BlockSummaryData getChainTipData() {
return this.chainTipData;
}
diff --git a/src/main/java/org/qortal/data/network/PeerChainTipData.java b/src/main/java/org/qortal/data/network/PeerChainTipData.java
deleted file mode 100644
index d8dbbad4..00000000
--- a/src/main/java/org/qortal/data/network/PeerChainTipData.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.qortal.data.network;
-
-public class PeerChainTipData {
-
- /** Latest block height as reported by peer. */
- private Integer lastHeight;
- /** Latest block signature as reported by peer. */
- private byte[] lastBlockSignature;
- /** Latest block timestamp as reported by peer. */
- private Long lastBlockTimestamp;
- /** Latest block minter public key as reported by peer. */
- private byte[] lastBlockMinter;
-
- public PeerChainTipData(Integer lastHeight, byte[] lastBlockSignature, Long lastBlockTimestamp, byte[] lastBlockMinter) {
- this.lastHeight = lastHeight;
- this.lastBlockSignature = lastBlockSignature;
- this.lastBlockTimestamp = lastBlockTimestamp;
- this.lastBlockMinter = lastBlockMinter;
- }
-
- public Integer getLastHeight() {
- return this.lastHeight;
- }
-
- public byte[] getLastBlockSignature() {
- return this.lastBlockSignature;
- }
-
- public Long getLastBlockTimestamp() {
- return this.lastBlockTimestamp;
- }
-
- public byte[] getLastBlockMinter() {
- return this.lastBlockMinter;
- }
-
-}
diff --git a/src/main/java/org/qortal/data/network/PeerData.java b/src/main/java/org/qortal/data/network/PeerData.java
index 09982c00..471685dd 100644
--- a/src/main/java/org/qortal/data/network/PeerData.java
+++ b/src/main/java/org/qortal/data/network/PeerData.java
@@ -28,6 +28,9 @@ public class PeerData {
private Long addedWhen;
private String addedBy;
+ /** The number of consecutive times we failed to sync with this peer */
+ private int failedSyncCount = 0;
+
// Constructors
// necessary for JAXB serialization
@@ -92,6 +95,18 @@ public class PeerData {
return this.addedBy;
}
+ public int getFailedSyncCount() {
+ return this.failedSyncCount;
+ }
+
+ public void setFailedSyncCount(int failedSyncCount) {
+ this.failedSyncCount = failedSyncCount;
+ }
+
+ public void incrementFailedSyncCount() {
+ this.failedSyncCount++;
+ }
+
// Pretty peerAddress getter for JAXB
@XmlElement(name = "address")
protected String getPrettyAddress() {
diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java
index 57073e99..8aac68f0 100644
--- a/src/main/java/org/qortal/network/Network.java
+++ b/src/main/java/org/qortal/network/Network.java
@@ -11,6 +11,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataFileListManager;
import org.qortal.controller.arbitrary.ArbitraryDataManager;
import org.qortal.crypto.Crypto;
import org.qortal.data.block.BlockData;
+import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.message.*;
@@ -90,6 +91,8 @@ public class Network {
private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds
+ private static final int BROADCAST_CHAIN_TIP_DEPTH = 7; // Just enough to fill a SINGLE TCP packet (~1440 bytes)
+
// Generate our node keys / ID
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
@@ -1087,10 +1090,16 @@ public class Network {
if (peer.isOutbound()) {
if (!Settings.getInstance().isLite()) {
- // Send our height
- Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
- if (!peer.sendMessage(heightMessage)) {
- peer.disconnect("failed to send height/info");
+ // Send our height / chain tip info
+ Message message = this.buildHeightOrChainTipInfo(peer);
+
+ if (message == null) {
+ peer.disconnect("Couldn't build our chain tip info");
+ return;
+ }
+
+ if (!peer.sendMessage(message)) {
+ peer.disconnect("failed to send height / chain tip info");
return;
}
}
@@ -1164,10 +1173,47 @@ public class Network {
return new PeersV2Message(peerAddresses);
}
- public Message buildHeightMessage(Peer peer, BlockData blockData) {
- // HEIGHT_V2 contains way more useful info
- return new HeightV2Message(blockData.getHeight(), blockData.getSignature(),
- blockData.getTimestamp(), blockData.getMinterPublicKey());
+ /** Builds either (legacy) HeightV2Message or (newer) BlockSummariesV2Message, depending on peer version.
+ *
+ * @return Message, or null if DataException was thrown.
+ */
+ public Message buildHeightOrChainTipInfo(Peer peer) {
+ if (peer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION) {
+ int latestHeight = Controller.getInstance().getChainHeight();
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ List latestBlockSummaries = repository.getBlockRepository().getBlockSummaries(latestHeight - BROADCAST_CHAIN_TIP_DEPTH, latestHeight);
+ return new BlockSummariesV2Message(latestBlockSummaries);
+ } catch (DataException e) {
+ return null;
+ }
+ } else {
+ // For older peers
+ BlockData latestBlockData = Controller.getInstance().getChainTip();
+ return new HeightV2Message(latestBlockData.getHeight(), latestBlockData.getSignature(),
+ latestBlockData.getTimestamp(), latestBlockData.getMinterPublicKey());
+ }
+ }
+
+ public void broadcastOurChain() {
+ BlockData latestBlockData = Controller.getInstance().getChainTip();
+ int latestHeight = latestBlockData.getHeight();
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ List latestBlockSummaries = repository.getBlockRepository().getBlockSummaries(latestHeight - BROADCAST_CHAIN_TIP_DEPTH, latestHeight);
+ Message latestBlockSummariesMessage = new BlockSummariesV2Message(latestBlockSummaries);
+
+ // For older peers
+ Message heightMessage = new HeightV2Message(latestBlockData.getHeight(), latestBlockData.getSignature(),
+ latestBlockData.getTimestamp(), latestBlockData.getMinterPublicKey());
+
+ Network.getInstance().broadcast(broadcastPeer -> broadcastPeer.getPeersVersion() >= BlockSummariesV2Message.MINIMUM_PEER_VERSION
+ ? latestBlockSummariesMessage
+ : heightMessage
+ );
+ } catch (DataException e) {
+ LOGGER.warn("Couldn't broadcast our chain tip info", e);
+ }
}
public Message buildNewTransactionMessage(Peer peer, TransactionData transactionData) {
diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java
index cac0ccc9..a187d29b 100644
--- a/src/main/java/org/qortal/network/Peer.java
+++ b/src/main/java/org/qortal/network/Peer.java
@@ -6,8 +6,8 @@ import com.google.common.net.InetAddresses;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.controller.Controller;
+import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.block.CommonBlockData;
-import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.network.PeerData;
import org.qortal.network.message.ChallengeMessage;
import org.qortal.network.message.Message;
@@ -148,7 +148,7 @@ public class Peer {
/**
* Latest block info as reported by peer.
*/
- private PeerChainTipData peersChainTipData;
+ private List peersChainTipData = Collections.emptyList();
/**
* Our common block with this peer
@@ -353,28 +353,34 @@ public class Peer {
}
}
- public PeerChainTipData getChainTipData() {
- synchronized (this.peerInfoLock) {
- return this.peersChainTipData;
- }
+ public BlockSummaryData getChainTipData() {
+ List chainTipSummaries = this.peersChainTipData;
+
+ if (chainTipSummaries.isEmpty())
+ return null;
+
+ // Return last entry, which should have greatest height
+ return chainTipSummaries.get(chainTipSummaries.size() - 1);
}
- public void setChainTipData(PeerChainTipData chainTipData) {
- synchronized (this.peerInfoLock) {
- this.peersChainTipData = chainTipData;
- }
+ public void setChainTipData(BlockSummaryData chainTipData) {
+ this.peersChainTipData = Collections.singletonList(chainTipData);
+ }
+
+ public List getChainTipSummaries() {
+ return this.peersChainTipData;
+ }
+
+ public void setChainTipSummaries(List chainTipSummaries) {
+ this.peersChainTipData = List.copyOf(chainTipSummaries);
}
public CommonBlockData getCommonBlockData() {
- synchronized (this.peerInfoLock) {
- return this.commonBlockData;
- }
+ return this.commonBlockData;
}
public void setCommonBlockData(CommonBlockData commonBlockData) {
- synchronized (this.peerInfoLock) {
- this.commonBlockData = commonBlockData;
- }
+ this.commonBlockData = commonBlockData;
}
public boolean isSyncInProgress() {
@@ -904,20 +910,22 @@ public class Peer {
// Common block data
public boolean canUseCachedCommonBlockData() {
- PeerChainTipData peerChainTipData = this.getChainTipData();
- CommonBlockData commonBlockData = this.getCommonBlockData();
+ BlockSummaryData peerChainTipData = this.getChainTipData();
+ if (peerChainTipData == null || peerChainTipData.getSignature() == null)
+ return false;
- if (peerChainTipData != null && commonBlockData != null) {
- PeerChainTipData commonBlockChainTipData = commonBlockData.getChainTipData();
- if (peerChainTipData.getLastBlockSignature() != null && commonBlockChainTipData != null
- && commonBlockChainTipData.getLastBlockSignature() != null) {
- if (Arrays.equals(peerChainTipData.getLastBlockSignature(),
- commonBlockChainTipData.getLastBlockSignature())) {
- return true;
- }
- }
- }
- return false;
+ CommonBlockData commonBlockData = this.getCommonBlockData();
+ if (commonBlockData == null)
+ return false;
+
+ BlockSummaryData commonBlockChainTipData = commonBlockData.getChainTipData();
+ if (commonBlockChainTipData == null || commonBlockChainTipData.getSignature() == null)
+ return false;
+
+ if (!Arrays.equals(peerChainTipData.getSignature(), commonBlockChainTipData.getSignature()))
+ return false;
+
+ return true;
}
diff --git a/src/main/java/org/qortal/network/message/BlockSummariesV2Message.java b/src/main/java/org/qortal/network/message/BlockSummariesV2Message.java
new file mode 100644
index 00000000..62428cc0
--- /dev/null
+++ b/src/main/java/org/qortal/network/message/BlockSummariesV2Message.java
@@ -0,0 +1,109 @@
+package org.qortal.network.message;
+
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+import org.qortal.data.block.BlockSummaryData;
+import org.qortal.transform.Transformer;
+import org.qortal.transform.block.BlockTransformer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BlockSummariesV2Message extends Message {
+
+ public static final long MINIMUM_PEER_VERSION = 0x0300060001L;
+
+ private static final int BLOCK_SUMMARY_V2_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH /* block signature */
+ + Transformer.PUBLIC_KEY_LENGTH /* minter public key */
+ + Transformer.INT_LENGTH /* online accounts count */
+ + Transformer.LONG_LENGTH /* block timestamp */
+ + Transformer.INT_LENGTH /* transactions count */
+ + BlockTransformer.BLOCK_SIGNATURE_LENGTH; /* block reference */
+
+ private List blockSummaries;
+
+ public BlockSummariesV2Message(List blockSummaries) {
+ super(MessageType.BLOCK_SUMMARIES_V2);
+
+ // Shortcut for when there are no summaries
+ if (blockSummaries.isEmpty()) {
+ this.dataBytes = Message.EMPTY_DATA_BYTES;
+ return;
+ }
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ // First summary's height
+ bytes.write(Ints.toByteArray(blockSummaries.get(0).getHeight()));
+
+ for (BlockSummaryData blockSummary : blockSummaries) {
+ bytes.write(blockSummary.getSignature());
+ bytes.write(blockSummary.getMinterPublicKey());
+ bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
+ bytes.write(Longs.toByteArray(blockSummary.getTimestamp()));
+ bytes.write(Ints.toByteArray(blockSummary.getTransactionCount()));
+ bytes.write(blockSummary.getReference());
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ }
+
+ private BlockSummariesV2Message(int id, List blockSummaries) {
+ super(id, MessageType.BLOCK_SUMMARIES_V2);
+
+ this.blockSummaries = blockSummaries;
+ }
+
+ public List getBlockSummaries() {
+ return this.blockSummaries;
+ }
+
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ List blockSummaries = new ArrayList<>();
+
+ // If there are no bytes remaining then we can treat this as an empty array of summaries
+ if (bytes.remaining() == 0)
+ return new BlockSummariesV2Message(id, blockSummaries);
+
+ int height = bytes.getInt();
+
+ // Expecting bytes remaining to be exact multiples of BLOCK_SUMMARY_V2_LENGTH
+ if (bytes.remaining() % BLOCK_SUMMARY_V2_LENGTH != 0)
+ throw new BufferUnderflowException();
+
+ while (bytes.hasRemaining()) {
+ byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
+ bytes.get(signature);
+
+ byte[] minterPublicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
+ bytes.get(minterPublicKey);
+
+ int onlineAccountsCount = bytes.getInt();
+
+ long timestamp = bytes.getLong();
+
+ int transactionsCount = bytes.getInt();
+
+ byte[] reference = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
+ bytes.get(reference);
+
+ BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey,
+ onlineAccountsCount, timestamp, transactionsCount, reference);
+ blockSummaries.add(blockSummary);
+
+ height++;
+ }
+
+ return new BlockSummariesV2Message(id, blockSummaries);
+ }
+
+}
diff --git a/src/main/java/org/qortal/network/message/GenericUnknownMessage.java b/src/main/java/org/qortal/network/message/GenericUnknownMessage.java
new file mode 100644
index 00000000..dea9f2b8
--- /dev/null
+++ b/src/main/java/org/qortal/network/message/GenericUnknownMessage.java
@@ -0,0 +1,23 @@
+package org.qortal.network.message;
+
+import java.nio.ByteBuffer;
+
+public class GenericUnknownMessage extends Message {
+
+ public static final long MINIMUM_PEER_VERSION = 0x0300060001L;
+
+ public GenericUnknownMessage() {
+ super(MessageType.GENERIC_UNKNOWN);
+
+ this.dataBytes = EMPTY_DATA_BYTES;
+ }
+
+ private GenericUnknownMessage(int id) {
+ super(id, MessageType.GENERIC_UNKNOWN);
+ }
+
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ return new GenericUnknownMessage(id);
+ }
+
+}
diff --git a/src/main/java/org/qortal/network/message/MessageType.java b/src/main/java/org/qortal/network/message/MessageType.java
index 087e7fbf..4dd4a3c8 100644
--- a/src/main/java/org/qortal/network/message/MessageType.java
+++ b/src/main/java/org/qortal/network/message/MessageType.java
@@ -21,6 +21,7 @@ public enum MessageType {
HEIGHT_V2(10, HeightV2Message::fromByteBuffer),
PING(11, PingMessage::fromByteBuffer),
PONG(12, PongMessage::fromByteBuffer),
+ GENERIC_UNKNOWN(13, GenericUnknownMessage::fromByteBuffer),
// Requesting data
PEERS_V2(20, PeersV2Message::fromByteBuffer),
@@ -41,6 +42,7 @@ public enum MessageType {
BLOCK_SUMMARIES(70, BlockSummariesMessage::fromByteBuffer),
GET_BLOCK_SUMMARIES(71, GetBlockSummariesMessage::fromByteBuffer),
+ BLOCK_SUMMARIES_V2(72, BlockSummariesV2Message::fromByteBuffer),
ONLINE_ACCOUNTS(80, OnlineAccountsMessage::fromByteBuffer),
GET_ONLINE_ACCOUNTS(81, GetOnlineAccountsMessage::fromByteBuffer),
diff --git a/src/main/java/org/qortal/network/message/OnlineAccountsV3Message.java b/src/main/java/org/qortal/network/message/OnlineAccountsV3Message.java
index 0c5f6730..d554d96c 100644
--- a/src/main/java/org/qortal/network/message/OnlineAccountsV3Message.java
+++ b/src/main/java/org/qortal/network/message/OnlineAccountsV3Message.java
@@ -20,7 +20,7 @@ import java.util.Map;
*/
public class OnlineAccountsV3Message extends Message {
- public static final long MIN_PEER_VERSION = 0x300050001L; // 3.5.1
+ public static final long MIN_PEER_VERSION = 0x300060000L; // 3.6.0
private List onlineAccounts;
diff --git a/src/main/java/org/qortal/repository/ChatRepository.java b/src/main/java/org/qortal/repository/ChatRepository.java
index cd4b9a8f..2ecd8a34 100644
--- a/src/main/java/org/qortal/repository/ChatRepository.java
+++ b/src/main/java/org/qortal/repository/ChatRepository.java
@@ -14,7 +14,7 @@ public interface ChatRepository {
* Expects EITHER non-null txGroupID OR non-null sender and recipient addresses.
*/
public List getMessagesMatchingCriteria(Long before, Long after,
- Integer txGroupId, List involving,
+ Integer txGroupId, byte[] reference, List involving,
Integer limit, Integer offset, Boolean reverse) throws DataException;
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException;
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockArchiveRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockArchiveRepository.java
index cc7e1611..c3c5638a 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockArchiveRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockArchiveRepository.java
@@ -143,13 +143,17 @@ public class HSQLDBBlockArchiveRepository implements BlockArchiveRepository {
byte[] blockMinterPublicKey = resultSet.getBytes(3);
// Fetch additional info from the archive itself
- int onlineAccountsCount = 0;
+ Integer onlineAccountsCount = null;
+ Long timestamp = null;
+ Integer transactionCount = null;
+ byte[] reference = null;
+
BlockData blockData = this.fromSignature(signature);
if (blockData != null) {
onlineAccountsCount = blockData.getOnlineAccountsCount();
}
- BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount);
+ BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount, timestamp, transactionCount, reference);
blockSummaries.add(blockSummary);
} while (resultSet.next());
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java
index b8238085..f38d549c 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java
@@ -297,7 +297,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public List getBlockSummariesBySigner(byte[] signerPublicKey, Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(512);
- sql.append("SELECT signature, height, Blocks.minter, online_accounts_count FROM ");
+ sql.append("SELECT signature, height, Blocks.minter, online_accounts_count, minted_when, transaction_count, Blocks.reference FROM ");
// List of minter account's public key and reward-share public keys with minter's public key
sql.append("(SELECT * FROM (VALUES (CAST(? AS QortalPublicKey))) UNION (SELECT reward_share_public_key FROM RewardShares WHERE minter_public_key = ?)) AS PublicKeys (public_key) ");
@@ -322,8 +322,12 @@ public class HSQLDBBlockRepository implements BlockRepository {
int height = resultSet.getInt(2);
byte[] blockMinterPublicKey = resultSet.getBytes(3);
int onlineAccountsCount = resultSet.getInt(4);
+ long timestamp = resultSet.getLong(5);
+ int transactionCount = resultSet.getInt(6);
+ byte[] reference = resultSet.getBytes(7);
- BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount);
+ BlockSummaryData blockSummary = new BlockSummaryData(height, signature, blockMinterPublicKey, onlineAccountsCount,
+ timestamp, transactionCount, reference);
blockSummaries.add(blockSummary);
} while (resultSet.next());
@@ -355,7 +359,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public List getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException {
- String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count "
+ String sql = "SELECT signature, height, minter, online_accounts_count, minted_when, transaction_count, reference "
+ "FROM Blocks WHERE height BETWEEN ? AND ?";
List blockSummaries = new ArrayList<>();
@@ -371,9 +375,10 @@ public class HSQLDBBlockRepository implements BlockRepository {
int onlineAccountsCount = resultSet.getInt(4);
long timestamp = resultSet.getLong(5);
int transactionCount = resultSet.getInt(6);
+ byte[] reference = resultSet.getBytes(7);
BlockSummaryData blockSummary = new BlockSummaryData(height, signature, minterPublicKey, onlineAccountsCount,
- timestamp, transactionCount);
+ timestamp, transactionCount, reference);
blockSummaries.add(blockSummary);
} while (resultSet.next());
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java
index 2972e9f2..2f570686 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBChatRepository.java
@@ -23,7 +23,7 @@ public class HSQLDBChatRepository implements ChatRepository {
}
@Override
- public List getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId,
+ public List getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes,
List involving, Integer limit, Integer offset, Boolean reverse)
throws DataException {
// Check args meet expectations
@@ -57,6 +57,11 @@ public class HSQLDBChatRepository implements ChatRepository {
bindParams.add(after);
}
+ if (referenceBytes != null) {
+ whereClauses.add("reference = ?");
+ bindParams.add(referenceBytes);
+ }
+
if (txGroupId != null) {
whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally
whereClauses.add("recipient IS NULL");
diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java
index 5b8d609e..acfd0e78 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -184,6 +184,8 @@ public class Settings {
// Peer-to-peer related
private boolean isTestNet = false;
+ /** Single node testnet mode */
+ private boolean singleNodeTestnet = false;
/** Port number for inbound peer-to-peer connections. */
private Integer listenPort;
/** Whether to attempt to open the listen port via UPnP */
@@ -203,8 +205,11 @@ public class Settings {
/** Maximum number of retry attempts if a peer fails to respond with the requested data */
private int maxRetries = 2;
+ /** The number of seconds of no activity before recovery mode begins */
+ public long recoveryModeTimeout = 10 * 60 * 1000L;
+
/** Minimum peer version number required in order to sync with them */
- private String minPeerVersion = "3.3.7";
+ private String minPeerVersion = "3.6.3";
/** 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 */
@@ -290,10 +295,6 @@ public class Settings {
/** Additional offset added to values returned by NTP.getTime() */
private Long testNtpOffset = null;
- // Online accounts
-
- /** Whether to opt-in to mempow computations for online accounts, ahead of general release */
- private boolean onlineAccountsMemPoWEnabled = false;
/* Foreign chains */
@@ -490,7 +491,7 @@ public class Settings {
private void validate() {
// Validation goes here
- if (this.minBlockchainPeers < 1)
+ if (this.minBlockchainPeers < 1 && !singleNodeTestnet)
throwValidationError("minBlockchainPeers must be at least 1");
if (this.apiKey != null && this.apiKey.trim().length() < 8)
@@ -647,6 +648,10 @@ public class Settings {
return this.isTestNet;
}
+ public boolean isSingleNodeTestnet() {
+ return this.singleNodeTestnet;
+ }
+
public int getListenPort() {
if (this.listenPort != null)
return this.listenPort;
@@ -667,6 +672,9 @@ public class Settings {
}
public int getMinBlockchainPeers() {
+ if (singleNodeTestnet)
+ return 0;
+
return this.minBlockchainPeers;
}
@@ -692,6 +700,10 @@ public class Settings {
public int getMaxRetries() { return this.maxRetries; }
+ public long getRecoveryModeTimeout() {
+ return recoveryModeTimeout;
+ }
+
public String getMinPeerVersion() { return this.minPeerVersion; }
public boolean getAllowConnectionsWithOlderPeerVersions() { return this.allowConnectionsWithOlderPeerVersions; }
@@ -800,10 +812,6 @@ public class Settings {
return this.testNtpOffset;
}
- public boolean isOnlineAccountsMemPoWEnabled() {
- return this.onlineAccountsMemPoWEnabled;
- }
-
public long getRepositoryBackupInterval() {
return this.repositoryBackupInterval;
}
diff --git a/src/main/java/org/qortal/transform/block/BlockTransformer.java b/src/main/java/org/qortal/transform/block/BlockTransformer.java
index 9e02a6f5..c97aa090 100644
--- a/src/main/java/org/qortal/transform/block/BlockTransformer.java
+++ b/src/main/java/org/qortal/transform/block/BlockTransformer.java
@@ -235,7 +235,7 @@ public class BlockTransformer extends Transformer {
// Online accounts timestamp is only present if there are also signatures
onlineAccountsTimestamp = byteBuffer.getLong();
- final int signaturesByteLength = getOnlineAccountSignaturesLength(onlineAccountsSignaturesCount, onlineAccountsCount, timestamp);
+ final int signaturesByteLength = (onlineAccountsSignaturesCount * Transformer.SIGNATURE_LENGTH) + (onlineAccountsCount * INT_LENGTH);
if (signaturesByteLength > BlockChain.getInstance().getMaxBlockSize())
throw new TransformationException("Byte data too long for online accounts signatures");
@@ -511,16 +511,6 @@ public class BlockTransformer extends Transformer {
return nonces;
}
- public static int getOnlineAccountSignaturesLength(int onlineAccountsSignaturesCount, int onlineAccountCount, long blockTimestamp) {
- if (blockTimestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp()) {
- // Once mempow is active, we expect the online account signatures to be appended with the nonce values
- return (onlineAccountsSignaturesCount * Transformer.SIGNATURE_LENGTH) + (onlineAccountCount * INT_LENGTH);
- }
- else {
- // Before mempow, only the online account signatures were included (which will likely be a single signature)
- return onlineAccountsSignaturesCount * Transformer.SIGNATURE_LENGTH;
- }
- }
public static byte[] extract(byte[] input, int pos, int length) {
byte[] output = new byte[length];
diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json
index 8d1600ed..34671c76 100644
--- a/src/main/resources/blockchain.json
+++ b/src/main/resources/blockchain.json
@@ -24,7 +24,6 @@
"onlineAccountSignaturesMinLifetime": 43200000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"onlineAccountsModulusV2Timestamp": 1659801600000,
- "onlineAccountsMemoryPoWTimestamp": 9999999999999,
"rewardsByHeight": [
{ "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 },
@@ -56,7 +55,7 @@
],
"qoraHoldersShareByHeight": [
{ "height": 1, "share": 0.20 },
- { "height": 9999999, "share": 0.01 }
+ { "height": 1010000, "share": 0.01 }
],
"qoraPerQortReward": 250,
"minAccountsToActivateShareBin": 30,
@@ -75,12 +74,13 @@
"atFindNextTransactionFix": 275000,
"newBlockSigHeight": 320000,
"shareBinFix": 399000,
- "sharesByLevelV2Height": 9999999,
+ "sharesByLevelV2Height": 1010000,
"rewardShareLimitTimestamp": 1657382400000,
"calcChainWeightTimestamp": 1620579600000,
"transactionV5Timestamp": 1642176000000,
"transactionV6Timestamp": 9999999999999,
- "disableReferenceTimestamp": 1655222400000
+ "disableReferenceTimestamp": 1655222400000,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/main/resources/i18n/SysTray_de.properties b/src/main/resources/i18n/SysTray_de.properties
index b949ca8c..4dc7edd2 100644
--- a/src/main/resources/i18n/SysTray_de.properties
+++ b/src/main/resources/i18n/SysTray_de.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automatisches Update
BLOCK_HEIGHT = height
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Build-Version
CHECK_TIME_ACCURACY = Prüfe Zeitgenauigkeit
diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties
index 204f0df2..39940be0 100644
--- a/src/main/resources/i18n/SysTray_en.properties
+++ b/src/main/resources/i18n/SysTray_en.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Auto Update
BLOCK_HEIGHT = height
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Build version
CHECK_TIME_ACCURACY = Check time accuracy
diff --git a/src/main/resources/i18n/SysTray_es.properties b/src/main/resources/i18n/SysTray_es.properties
index d4b931d4..36cbb22c 100644
--- a/src/main/resources/i18n/SysTray_es.properties
+++ b/src/main/resources/i18n/SysTray_es.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Actualización automática
BLOCK_HEIGHT = altura
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Versión de compilación
CHECK_TIME_ACCURACY = Comprobar la precisión del tiempo
diff --git a/src/main/resources/i18n/SysTray_fi.properties b/src/main/resources/i18n/SysTray_fi.properties
index bc787715..4038d615 100644
--- a/src/main/resources/i18n/SysTray_fi.properties
+++ b/src/main/resources/i18n/SysTray_fi.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automaattinen päivitys
BLOCK_HEIGHT = korkeus
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Versio
CHECK_TIME_ACCURACY = Tarkista ajan tarkkuus
diff --git a/src/main/resources/i18n/SysTray_fr.properties b/src/main/resources/i18n/SysTray_fr.properties
index 6e60713c..2e376842 100644
--- a/src/main/resources/i18n/SysTray_fr.properties
+++ b/src/main/resources/i18n/SysTray_fr.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Mise à jour automatique
BLOCK_HEIGHT = hauteur
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Numéro de version
CHECK_TIME_ACCURACY = Vérifier l'heure
diff --git a/src/main/resources/i18n/SysTray_hu.properties b/src/main/resources/i18n/SysTray_hu.properties
index 9bc51ff5..74ab21ac 100644
--- a/src/main/resources/i18n/SysTray_hu.properties
+++ b/src/main/resources/i18n/SysTray_hu.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automatikus Frissítés
BLOCK_HEIGHT = blokkmagasság
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Verzió
CHECK_TIME_ACCURACY = Óra pontosságának ellenőrzése
diff --git a/src/main/resources/i18n/SysTray_it.properties b/src/main/resources/i18n/SysTray_it.properties
index bf61cc46..d966d825 100644
--- a/src/main/resources/i18n/SysTray_it.properties
+++ b/src/main/resources/i18n/SysTray_it.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Aggiornamento automatico
BLOCK_HEIGHT = altezza
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Versione
CHECK_TIME_ACCURACY = Controlla la precisione dell'ora
diff --git a/src/main/resources/i18n/SysTray_ko.properties b/src/main/resources/i18n/SysTray_ko.properties
index 9773a54f..dc6cb69b 100644
--- a/src/main/resources/i18n/SysTray_ko.properties
+++ b/src/main/resources/i18n/SysTray_ko.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = 자동 업데이트
BLOCK_HEIGHT = 높이
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = 빌드 버전
CHECK_TIME_ACCURACY = 시간 정확도 점검
diff --git a/src/main/resources/i18n/SysTray_nl.properties b/src/main/resources/i18n/SysTray_nl.properties
index 8a4f112b..c2acb7ce 100644
--- a/src/main/resources/i18n/SysTray_nl.properties
+++ b/src/main/resources/i18n/SysTray_nl.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automatische Update
BLOCK_HEIGHT = Block hoogte
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Versie nummer
CHECK_TIME_ACCURACY = Controleer accuraatheid van de tijd
diff --git a/src/main/resources/i18n/SysTray_ro.properties b/src/main/resources/i18n/SysTray_ro.properties
index 0e1aa6c6..4130bbcb 100644
--- a/src/main/resources/i18n/SysTray_ro.properties
+++ b/src/main/resources/i18n/SysTray_ro.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Actualizare automata
BLOCK_HEIGHT = dimensiune
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = versiunea compilatiei
CHECK_TIME_ACCURACY = verificare exactitate ora
diff --git a/src/main/resources/i18n/SysTray_ru.properties b/src/main/resources/i18n/SysTray_ru.properties
index fc3d8648..ff346304 100644
--- a/src/main/resources/i18n/SysTray_ru.properties
+++ b/src/main/resources/i18n/SysTray_ru.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Автоматическое обновление
BLOCK_HEIGHT = Высота блока
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Версия сборки
CHECK_TIME_ACCURACY = Проверка точного времени
diff --git a/src/main/resources/i18n/SysTray_sv.properties b/src/main/resources/i18n/SysTray_sv.properties
index 0e74337b..96f291b5 100644
--- a/src/main/resources/i18n/SysTray_sv.properties
+++ b/src/main/resources/i18n/SysTray_sv.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = Automatisk uppdatering
BLOCK_HEIGHT = höjd
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = Byggversion
CHECK_TIME_ACCURACY = Kontrollera tidens noggrannhet
diff --git a/src/main/resources/i18n/SysTray_zh_CN.properties b/src/main/resources/i18n/SysTray_zh_CN.properties
index c103d24b..d6848a7c 100644
--- a/src/main/resources/i18n/SysTray_zh_CN.properties
+++ b/src/main/resources/i18n/SysTray_zh_CN.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = 自动更新
BLOCK_HEIGHT = 区块高度
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = 版本
CHECK_TIME_ACCURACY = 检查时间准确性
diff --git a/src/main/resources/i18n/SysTray_zh_TW.properties b/src/main/resources/i18n/SysTray_zh_TW.properties
index 5e6ccc3e..eabdbb63 100644
--- a/src/main/resources/i18n/SysTray_zh_TW.properties
+++ b/src/main/resources/i18n/SysTray_zh_TW.properties
@@ -7,6 +7,8 @@ AUTO_UPDATE = 自動更新
BLOCK_HEIGHT = 區塊高度
+BLOCKS_REMAINING = blocks remaining
+
BUILD_VERSION = 版本
CHECK_TIME_ACCURACY = 檢查時間準確性
diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java
index 4db8bdc7..e6a51776 100644
--- a/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java
+++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryServiceTests.java
@@ -102,77 +102,77 @@ public class ArbitraryServiceTests extends Common {
}
@Test
- public void testValidQortalMetadata() throws IOException {
- // Metadata is to describe an arbitrary resource (title, description, tags, etc)
- String dataString = "{\"title\":\"Test Title\", \"description\":\"Test description\", \"tags\":[\"test\"]}";
+ public void testValidateGifRepository() throws IOException {
+ // Generate some random data
+ byte[] data = new byte[1024];
+ new Random().nextBytes(data);
- // Write to temp path
- Path path = Files.createTempFile("testValidQortalMetadata", null);
+ // Write the data to several files in a temp path
+ Path path = Files.createTempDirectory("testValidateGifRepository");
path.toFile().deleteOnExit();
- Files.write(path, dataString.getBytes(), StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image3.gif"), data, StandardOpenOption.CREATE);
- Service service = Service.QORTAL_METADATA;
+ Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
+
+ // There is an index file in the root
assertEquals(ValidationResult.OK, service.validate(path));
}
@Test
- public void testQortalMetadataMissingKeys() throws IOException {
- // Metadata is to describe an arbitrary resource (title, description, tags, etc)
- String dataString = "{\"description\":\"Test description\", \"tags\":[\"test\"]}";
+ public void testValidateMultiLayerGifRepository() throws IOException {
+ // Generate some random data
+ byte[] data = new byte[1024];
+ new Random().nextBytes(data);
- // Write to temp path
- Path path = Files.createTempFile("testQortalMetadataMissingKeys", null);
+ // Write the data to several files in a temp path
+ Path path = Files.createTempDirectory("testValidateMultiLayerGifRepository");
path.toFile().deleteOnExit();
- Files.write(path, dataString.getBytes(), StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
- Service service = Service.QORTAL_METADATA;
+ Path subdirectory = Paths.get(path.toString(), "subdirectory");
+ Files.createDirectories(subdirectory);
+ Files.write(Paths.get(subdirectory.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(subdirectory.toString(), "image3.gif"), data, StandardOpenOption.CREATE);
+
+ Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
- assertEquals(ValidationResult.MISSING_KEYS, service.validate(path));
+
+ // There is an index file in the root
+ assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
}
@Test
- public void testQortalMetadataTooLarge() throws IOException {
- // Metadata is to describe an arbitrary resource (title, description, tags, etc)
- String dataString = "{\"title\":\"Test Title\", \"description\":\"Test description\", \"tags\":[\"test\"]}";
+ public void testValidateEmptyGifRepository() throws IOException {
+ Path path = Files.createTempDirectory("testValidateEmptyGifRepository");
- // Generate some large data to go along with it
- int largeDataSize = 11*1024; // Larger than allowed 10kiB
- byte[] largeData = new byte[largeDataSize];
- new Random().nextBytes(largeData);
-
- // Write to temp path
- Path path = Files.createTempDirectory("testQortalMetadataTooLarge");
- path.toFile().deleteOnExit();
- Files.write(Paths.get(path.toString(), "data"), dataString.getBytes(), StandardOpenOption.CREATE);
- Files.write(Paths.get(path.toString(), "large_data"), largeData, StandardOpenOption.CREATE);
-
- Service service = Service.QORTAL_METADATA;
+ Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
- assertEquals(ValidationResult.EXCEEDS_SIZE_LIMIT, service.validate(path));
+
+ // There is an index file in the root
+ assertEquals(ValidationResult.MISSING_DATA, service.validate(path));
}
@Test
- public void testMultipleFileMetadata() throws IOException {
- // Metadata is to describe an arbitrary resource (title, description, tags, etc)
- String dataString = "{\"title\":\"Test Title\", \"description\":\"Test description\", \"tags\":[\"test\"]}";
+ public void testValidateInvalidGifRepository() throws IOException {
+ // Generate some random data
+ byte[] data = new byte[1024];
+ new Random().nextBytes(data);
- // Generate some large data to go along with it
- int otherDataSize = 1024; // Smaller than 10kiB limit
- byte[] otherData = new byte[otherDataSize];
- new Random().nextBytes(otherData);
-
- // Write to temp path
- Path path = Files.createTempDirectory("testMultipleFileMetadata");
+ // Write the data to several files in a temp path
+ Path path = Files.createTempDirectory("testValidateInvalidGifRepository");
path.toFile().deleteOnExit();
- Files.write(Paths.get(path.toString(), "data"), dataString.getBytes(), StandardOpenOption.CREATE);
- Files.write(Paths.get(path.toString(), "other_data"), otherData, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
+ Files.write(Paths.get(path.toString(), "image3.jpg"), data, StandardOpenOption.CREATE); // Invalid extension
- Service service = Service.QORTAL_METADATA;
+ Service service = Service.GIF_REPOSITORY;
assertTrue(service.isValidationRequired());
- // There are multiple files, so we don't know which one to parse as JSON
- assertEquals(ValidationResult.MISSING_KEYS, service.validate(path));
+ // There is an index file in the root
+ assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
}
}
diff --git a/src/test/java/org/qortal/test/common/AccountUtils.java b/src/test/java/org/qortal/test/common/AccountUtils.java
index 0d0b6d6a..0d8baae2 100644
--- a/src/test/java/org/qortal/test/common/AccountUtils.java
+++ b/src/test/java/org/qortal/test/common/AccountUtils.java
@@ -124,8 +124,6 @@ public class AccountUtils {
long timestamp = System.currentTimeMillis();
byte[] timestampBytes = Longs.toByteArray(timestamp);
- final boolean mempowActive = timestamp >= BlockChain.getInstance().getOnlineAccountsMemoryPoWTimestamp();
-
for (int a = 0; a < numAccounts; ++a) {
byte[] privateKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
SECURE_RANDOM.nextBytes(privateKey);
@@ -135,7 +133,7 @@ public class AccountUtils {
byte[] signature = signForAggregation(privateKey, timestampBytes);
- Integer nonce = mempowActive ? new Random().nextInt(500000) : null;
+ Integer nonce = new Random().nextInt(500000);
onlineAccounts.add(new OnlineAccountData(timestamp, signature, publicKey, nonce));
}
diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json
index 37224684..4a883bd9 100644
--- a/src/test/resources/test-chain-v2-block-timestamps.json
+++ b/src/test/resources/test-chain-v2-block-timestamps.json
@@ -69,7 +69,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 9999999999999,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json
index 7ea0b86d..e8fee5e0 100644
--- a/src/test/resources/test-chain-v2-disable-reference.json
+++ b/src/test/resources/test-chain-v2-disable-reference.json
@@ -72,7 +72,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 0
+ "disableReferenceTimestamp": 0,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json
index 85a50f83..17a713a0 100644
--- a/src/test/resources/test-chain-v2-founder-rewards.json
+++ b/src/test/resources/test-chain-v2-founder-rewards.json
@@ -73,7 +73,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json
index ebc3ccfa..b57c3195 100644
--- a/src/test/resources/test-chain-v2-leftover-reward.json
+++ b/src/test/resources/test-chain-v2-leftover-reward.json
@@ -73,7 +73,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json
index cc91f993..60b3cd76 100644
--- a/src/test/resources/test-chain-v2-minting.json
+++ b/src/test/resources/test-chain-v2-minting.json
@@ -73,7 +73,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json
index 085d1dbf..2d044687 100644
--- a/src/test/resources/test-chain-v2-qora-holder-extremes.json
+++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json
@@ -73,7 +73,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json
index 75858057..3cf8848e 100644
--- a/src/test/resources/test-chain-v2-qora-holder-reduction.json
+++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json
@@ -74,7 +74,8 @@
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
"disableReferenceTimestamp": 9999999999999,
- "aggregateSignatureTimestamp": 0
+ "aggregateSignatureTimestamp": 0,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json
index 0706c5bb..93965b76 100644
--- a/src/test/resources/test-chain-v2-qora-holder.json
+++ b/src/test/resources/test-chain-v2-qora-holder.json
@@ -73,7 +73,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json
index b3644d6b..06422e71 100644
--- a/src/test/resources/test-chain-v2-reward-levels.json
+++ b/src/test/resources/test-chain-v2-reward-levels.json
@@ -73,7 +73,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json
index 1c68dda4..6adcd0ac 100644
--- a/src/test/resources/test-chain-v2-reward-scaling.json
+++ b/src/test/resources/test-chain-v2-reward-scaling.json
@@ -73,7 +73,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json
index 10d2aab3..95324b56 100644
--- a/src/test/resources/test-chain-v2-reward-shares.json
+++ b/src/test/resources/test-chain-v2-reward-shares.json
@@ -73,7 +73,8 @@
"newConsensusTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json
index e3a2f4f2..84c692d5 100644
--- a/src/test/resources/test-chain-v2.json
+++ b/src/test/resources/test-chain-v2.json
@@ -73,7 +73,8 @@
"calcChainWeightTimestamp": 0,
"transactionV5Timestamp": 0,
"transactionV6Timestamp": 0,
- "disableReferenceTimestamp": 9999999999999
+ "disableReferenceTimestamp": 9999999999999,
+ "increaseOnlineAccountsDifficultyTimestamp": 9999999999999
},
"genesisInfo": {
"version": 4,
diff --git a/tools/build-zip.sh b/tools/build-zip.sh
index b52b5da7..f423bca1 100755
--- a/tools/build-zip.sh
+++ b/tools/build-zip.sh
@@ -58,6 +58,9 @@ git show HEAD:log4j2.properties > ${build_dir}/log4j2.properties
git show HEAD:start.sh > ${build_dir}/start.sh
git show HEAD:stop.sh > ${build_dir}/stop.sh
+chmod +x ${build_dir}/start.sh
+chmod +x ${build_dir}/stop.sh
+
printf "{\n}\n" > ${build_dir}/settings.json
gtouch -d ${commit_ts%%+??:??} ${build_dir} ${build_dir}/*